From 5c8fd1b7a4c5b4a9bcda4a51a8496dbd3a981f72 Mon Sep 17 00:00:00 2001 From: johnnyshields <27655+johnnyshields@users.noreply.github.com> Date: Mon, 8 Jul 2024 18:59:29 +0900 Subject: [PATCH] Remove OneLogin namespace --- CHANGELOG.md | 3 + README.md | 34 +- UPGRADING.md | 2 +- lib/onelogin/ruby-saml/attribute_service.rb | 102 +- lib/onelogin/ruby-saml/attributes.rb | 246 ++- lib/onelogin/ruby-saml/authrequest.rb | 296 ++- lib/onelogin/ruby-saml/error_handling.rb | 34 +- lib/onelogin/ruby-saml/http_error.rb | 7 +- lib/onelogin/ruby-saml/idp_metadata_parser.rb | 764 ++++---- lib/onelogin/ruby-saml/logging.rb | 40 +- lib/onelogin/ruby-saml/logoutrequest.rb | 248 ++- lib/onelogin/ruby-saml/logoutresponse.rb | 442 +++-- lib/onelogin/ruby-saml/metadata.rb | 266 ++- lib/onelogin/ruby-saml/response.rb | 1718 ++++++++--------- lib/onelogin/ruby-saml/saml_message.rb | 244 ++- lib/onelogin/ruby-saml/setting_error.rb | 8 +- lib/onelogin/ruby-saml/settings.rb | 621 +++--- lib/onelogin/ruby-saml/slo_logoutrequest.rb | 536 +++-- lib/onelogin/ruby-saml/slo_logoutresponse.rb | 256 ++- lib/onelogin/ruby-saml/utils.rb | 754 ++++---- lib/onelogin/ruby-saml/validation_error.rb | 7 +- lib/onelogin/ruby-saml/version.rb | 6 +- lib/xml_security.rb | 10 +- ruby-saml.gemspec | 2 +- test/attributes_test.rb | 2 +- test/idp_metadata_parser_test.rb | 76 +- test/logging_test.rb | 20 +- .../logoutresponse_fixtures.rb | 2 +- test/logoutrequest_test.rb | 78 +- test/logoutresponse_test.rb | 104 +- test/metadata_test.rb | 12 +- test/request_test.rb | 96 +- test/response_test.rb | 338 ++-- test/saml_message_test.rb | 14 +- test/settings_test.rb | 28 +- test/slo_logoutrequest_test.rb | 102 +- test/slo_logoutresponse_test.rb | 78 +- test/test_helper.rb | 4 +- test/utils_test.rb | 142 +- test/xml_security_test.rb | 54 +- 40 files changed, 3879 insertions(+), 3917 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 475797554..3f100c9c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Ruby SAML Changelog +### 2.0.0 +* Remove OneLogin namespace. The root namespace of the gem is now "RubySaml". + ### 1.17.0 * [#673](https://github.com/SAML-Toolkits/ruby-saml/pull/673) Add `Settings#sp_cert_multi` paramter to facilitate SP certificate and key rotation. * [#673](https://github.com/SAML-Toolkits/ruby-saml/pull/673) Support multiple simultaneous SP decryption keys via `Settings#sp_cert_multi` parameter. diff --git a/README.md b/README.md index ec714d515..466a7a3a7 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ can create an XML External Entity (XXE) vulnerability if the XML data is not tru However, ruby-saml never enables this dangerous Nokogiri configuration; ruby-saml never enables DTDLOAD, and it never disables NONET. -The OneLogin::RubySaml::IdpMetadataParser class does not validate in any way the URL +The RubySaml::IdpMetadataParser class does not validate in any way the URL that is introduced in order to be parsed. Usually the same administrator that handles the Service Provider also sets the URL to @@ -124,7 +124,7 @@ To override the default behavior and control the destination of log messages, pr a ruby Logger object to the gem's logging singleton: ```ruby -OneLogin::RubySaml::Logging.logger = Logger.new('/var/log/ruby-saml.log') +RubySaml::Logging.logger = Logger.new('/var/log/ruby-saml.log') ``` ## The Initialization Phase @@ -136,7 +136,7 @@ like this (ignore the saml_settings method call for now): ```ruby def init - request = OneLogin::RubySaml::Authrequest.new + request = RubySaml::Authrequest.new redirect_to(request.create(saml_settings)) end ``` @@ -145,7 +145,7 @@ If the SP knows who should be authenticated in the IdP, then can provide that in ```ruby def init - request = OneLogin::RubySaml::Authrequest.new + request = RubySaml::Authrequest.new saml_settings.name_identifier_value_requested = "testuser@example.com" saml_settings.name_identifier_format = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" redirect_to(request.create(saml_settings)) @@ -159,7 +159,7 @@ methods are specific to your application): ```ruby def consume - response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], :settings => saml_settings) + response = RubySaml::Response.new(params[:SAMLResponse], :settings => saml_settings) # We validate the SAML Response and check if the user already exists in the system if response.is_valid? @@ -178,7 +178,7 @@ This is all handled with how you specify the settings that are in play via the ` That could be implemented along the lines of this: ``` -response = OneLogin::RubySaml::Response.new(params[:SAMLResponse]) +response = RubySaml::Response.new(params[:SAMLResponse]) response.settings = saml_settings ``` @@ -190,7 +190,7 @@ If you don't know what expect, always use the former (set the settings on initia ```ruby def saml_settings - settings = OneLogin::RubySaml::Settings.new + settings = RubySaml::Settings.new settings.assertion_consumer_service_url = "http://#{request.host}/saml/consume" settings.sp_entity_id = "http://#{request.host}/saml/metadata" @@ -221,16 +221,16 @@ end The use of settings.issuer is deprecated in favour of settings.sp_entity_id since version 1.11.0 -Some assertion validations can be skipped by passing parameters to `OneLogin::RubySaml::Response.new()`. +Some assertion validations can be skipped by passing parameters to `RubySaml::Response.new()`. For example, you can skip the `AuthnStatement`, `Conditions`, `Recipient`, or the `SubjectConfirmation` validations by initializing the response with different options: ```ruby -response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], {skip_authnstatement: true}) # skips AuthnStatement -response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], {skip_conditions: true}) # skips conditions -response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], {skip_subject_confirmation: true}) # skips subject confirmation -response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], {skip_recipient_check: true}) # doesn't skip subject confirmation, but skips the recipient check which is a sub check of the subject_confirmation check -response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], {skip_audience: true}) # skips audience check +response = RubySaml::Response.new(params[:SAMLResponse], {skip_authnstatement: true}) # skips AuthnStatement +response = RubySaml::Response.new(params[:SAMLResponse], {skip_conditions: true}) # skips conditions +response = RubySaml::Response.new(params[:SAMLResponse], {skip_subject_confirmation: true}) # skips subject confirmation +response = RubySaml::Response.new(params[:SAMLResponse], {skip_recipient_check: true}) # doesn't skip subject confirmation, but skips the recipient check which is a sub check of the subject_confirmation check +response = RubySaml::Response.new(params[:SAMLResponse], {skip_audience: true}) # skips audience check ``` All that's left is to wrap everything in a controller and reference it in the initialization and @@ -240,12 +240,12 @@ consumption URLs in OneLogin. A full controller example could look like this: # This controller expects you to use the URLs /saml/init and /saml/consume in your OneLogin application. class SamlController < ApplicationController def init - request = OneLogin::RubySaml::Authrequest.new + request = RubySaml::Authrequest.new redirect_to(request.create(saml_settings)) end def consume - response = OneLogin::RubySaml::Response.new(params[:SAMLResponse]) + response = RubySaml::Response.new(params[:SAMLResponse]) response.settings = saml_settings # We validate the SAML Response and check if the user already exists in the system @@ -262,7 +262,7 @@ class SamlController < ApplicationController private def saml_settings - settings = OneLogin::RubySaml::Settings.new + settings = RubySaml::Settings.new settings.assertion_consumer_service_url = "http://#{request.host}/saml/consume" settings.sp_entity_id = "http://#{request.host}/saml/metadata" @@ -335,7 +335,7 @@ Using `IdpMetadataParser#parse_remote`, the IdP metadata will be added to the se ```ruby def saml_settings - idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + idp_metadata_parser = RubySaml::IdpMetadataParser.new # Returns OneLogin::RubySaml::Settings pre-populated with IdP metadata settings = idp_metadata_parser.parse_remote("https://example.com/auth/saml2/idp/metadata") diff --git a/UPGRADING.md b/UPGRADING.md index fab939700..908dbddcc 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -86,7 +86,7 @@ options = { "RelayState" => raw_query_params["RelayState"], }, } -slo_logout_request = OneLogin::RubySaml::SloLogoutrequest.new(query_params["SAMLRequest"], settings, options) +slo_logout_request = RubySaml::SloLogoutrequest.new(query_params["SAMLRequest"], settings, options) raise "Invalid Logout Request" unless slo_logout_request.is_valid? ``` diff --git a/lib/onelogin/ruby-saml/attribute_service.rb b/lib/onelogin/ruby-saml/attribute_service.rb index 4bc4284a3..3bc78acc3 100644 --- a/lib/onelogin/ruby-saml/attribute_service.rb +++ b/lib/onelogin/ruby-saml/attribute_service.rb @@ -1,59 +1,57 @@ # frozen_string_literal: true -module OneLogin - module RubySaml +module RubySaml - # SAML2 AttributeService. Auxiliary class to build the AttributeService of the SP Metadata + # SAML2 AttributeService. Auxiliary class to build the AttributeService of the SP Metadata + # + class AttributeService + attr_reader :attributes + attr_reader :name + attr_reader :index + + # Initializes the AttributeService, set the index value as 1 and an empty array as attributes + # + def initialize + @index = "1" + @attributes = [] + end + + def configure(&block) + instance_eval(&block) + end + + # @return [Boolean] True if the AttributeService object has been initialized and set with the required values + # (has attributes and a name) + def configured? + !@attributes.empty? && !@name.nil? + end + + # Set a name to the service + # @param name [String] The service name + # + def service_name(name) + @name = name + end + + # Set an index to the service + # @param index [Integer] An index + # + def service_index(index) + @index = index + end + + # Add an AttributeService + # @param options [Hash] AttributeService option values + # add_attribute( + # :name => "Name", + # :name_format => "Name Format", + # :index => 1, + # :friendly_name => "Friendly Name", + # :attribute_value => "Attribute Value" + # ) # - class AttributeService - attr_reader :attributes - attr_reader :name - attr_reader :index - - # Initializes the AttributeService, set the index value as 1 and an empty array as attributes - # - def initialize - @index = "1" - @attributes = [] - end - - def configure(&block) - instance_eval(&block) - end - - # @return [Boolean] True if the AttributeService object has been initialized and set with the required values - # (has attributes and a name) - def configured? - !@attributes.empty? && !@name.nil? - end - - # Set a name to the service - # @param name [String] The service name - # - def service_name(name) - @name = name - end - - # Set an index to the service - # @param index [Integer] An index - # - def service_index(index) - @index = index - end - - # Add an AttributeService - # @param options [Hash] AttributeService option values - # add_attribute( - # :name => "Name", - # :name_format => "Name Format", - # :index => 1, - # :friendly_name => "Friendly Name", - # :attribute_value => "Attribute Value" - # ) - # - def add_attribute(options={}) - attributes << options - end + def add_attribute(options={}) + attributes << options end end end diff --git a/lib/onelogin/ruby-saml/attributes.rb b/lib/onelogin/ruby-saml/attributes.rb index 51a47b93e..b4c1dbe3b 100644 --- a/lib/onelogin/ruby-saml/attributes.rb +++ b/lib/onelogin/ruby-saml/attributes.rb @@ -1,150 +1,148 @@ # frozen_string_literal: true -module OneLogin - module RubySaml +module RubySaml - # SAML2 Attributes. Parse the Attributes from the AttributeStatement of the SAML Response. - # - class Attributes - include Enumerable + # SAML2 Attributes. Parse the Attributes from the AttributeStatement of the SAML Response. + # + class Attributes + include Enumerable - attr_reader :attributes + attr_reader :attributes - # By default Attributes#[] is backwards compatible and - # returns only the first value for the attribute - # Setting this to `false` returns all values for an attribute - @@single_value_compatibility = true + # By default Attributes#[] is backwards compatible and + # returns only the first value for the attribute + # Setting this to `false` returns all values for an attribute + @@single_value_compatibility = true - # @return [Boolean] Get current status of backwards compatibility mode. - # - def self.single_value_compatibility - @@single_value_compatibility - end + # @return [Boolean] Get current status of backwards compatibility mode. + # + def self.single_value_compatibility + @@single_value_compatibility + end - # Sets the backwards compatibility mode on/off. - # @param value [Boolean] - # - def self.single_value_compatibility=(value) - @@single_value_compatibility = value - end + # Sets the backwards compatibility mode on/off. + # @param value [Boolean] + # + def self.single_value_compatibility=(value) + @@single_value_compatibility = value + end - # @param attrs [Hash] The +attrs+ must be a Hash with attribute names as keys and **arrays** as values: - # Attributes.new({ - # 'name' => ['value1', 'value2'], - # 'mail' => ['value1'], - # }) - # - def initialize(attrs = {}) - @attributes = attrs - end + # @param attrs [Hash] The +attrs+ must be a Hash with attribute names as keys and **arrays** as values: + # Attributes.new({ + # 'name' => ['value1', 'value2'], + # 'mail' => ['value1'], + # }) + # + def initialize(attrs = {}) + @attributes = attrs + end - # Iterate over all attributes - # - def each(&block) - attributes.each(&block) - end + # Iterate over all attributes + # + def each(&block) + attributes.each(&block) + end - # Test attribute presence by name - # @param name [String] The attribute name to be checked - # - def include?(name) - attributes.key?(canonize_name(name)) - end - - # Return first value for an attribute - # @param name [String] The attribute name - # @return [String] The value (First occurrence) - # - def single(name) - attributes[canonize_name(name)].first if include?(name) - end + # Test attribute presence by name + # @param name [String] The attribute name to be checked + # + def include?(name) + attributes.key?(canonize_name(name)) + end - # Return all values for an attribute - # @param name [String] The attribute name - # @return [Array] Values of the attribute - # - def multi(name) - attributes[canonize_name(name)] - end + # Return first value for an attribute + # @param name [String] The attribute name + # @return [String] The value (First occurrence) + # + def single(name) + attributes[canonize_name(name)].first if include?(name) + end - # Retrieve attribute value(s) - # @param name [String] The attribute name - # @return [String|Array] Depending on the single value compatibility status this returns: - # - First value if single_value_compatibility = true - # response.attributes['mail'] # => 'user@example.com' - # - All values if single_value_compatibility = false - # response.attributes['mail'] # => ['user@example.com','user@example.net'] - # - def [](name) - self.class.single_value_compatibility ? single(canonize_name(name)) : multi(canonize_name(name)) - end + # Return all values for an attribute + # @param name [String] The attribute name + # @return [Array] Values of the attribute + # + def multi(name) + attributes[canonize_name(name)] + end - # @return [Hash] Return all attributes as a hash - # - def all - attributes - end + # Retrieve attribute value(s) + # @param name [String] The attribute name + # @return [String|Array] Depending on the single value compatibility status this returns: + # - First value if single_value_compatibility = true + # response.attributes['mail'] # => 'user@example.com' + # - All values if single_value_compatibility = false + # response.attributes['mail'] # => ['user@example.com','user@example.net'] + # + def [](name) + self.class.single_value_compatibility ? single(canonize_name(name)) : multi(canonize_name(name)) + end - # @param name [String] The attribute name - # @param values [Array] The values - # - def set(name, values) - attributes[canonize_name(name)] = values - end - alias_method :[]=, :set - - # @param name [String] The attribute name - # @param values [Array] The values - # - def add(name, values = []) - attributes[canonize_name(name)] ||= [] - attributes[canonize_name(name)] += Array(values) - end + # @return [Hash] Return all attributes as a hash + # + def all + attributes + end - # Make comparable to another Attributes collection based on attributes - # @param other [Attributes] An Attributes object to compare with - # @return [Boolean] True if are contains the same attributes and values - # - def ==(other) - if other.is_a?(Attributes) - all == other.all - else - super - end + # @param name [String] The attribute name + # @param values [Array] The values + # + def set(name, values) + attributes[canonize_name(name)] = values + end + alias_method :[]=, :set + + # @param name [String] The attribute name + # @param values [Array] The values + # + def add(name, values = []) + attributes[canonize_name(name)] ||= [] + attributes[canonize_name(name)] += Array(values) + end + + # Make comparable to another Attributes collection based on attributes + # @param other [Attributes] An Attributes object to compare with + # @return [Boolean] True if are contains the same attributes and values + # + def ==(other) + if other.is_a?(Attributes) + all == other.all + else + super end + end - # Fetch attribute value using name or regex - # @param name [String|Regexp] The attribute name - # @return [String|Array] Depending on the single value compatibility status this returns: - # - First value if single_value_compatibility = true - # response.attributes['mail'] # => 'user@example.com' - # - All values if single_value_compatibility = false - # response.attributes['mail'] # => ['user@example.com','user@example.net'] - # - def fetch(name) - attributes.each_key do |attribute_key| - if name.is_a?(Regexp) - if name.respond_to? :match? - return self[attribute_key] if name.match?(attribute_key) - elsif name.match(attribute_key) - return self[attribute_key] - end - elsif canonize_name(name) == canonize_name(attribute_key) + # Fetch attribute value using name or regex + # @param name [String|Regexp] The attribute name + # @return [String|Array] Depending on the single value compatibility status this returns: + # - First value if single_value_compatibility = true + # response.attributes['mail'] # => 'user@example.com' + # - All values if single_value_compatibility = false + # response.attributes['mail'] # => ['user@example.com','user@example.net'] + # + def fetch(name) + attributes.each_key do |attribute_key| + if name.is_a?(Regexp) + if name.respond_to? :match? + return self[attribute_key] if name.match?(attribute_key) + elsif name.match(attribute_key) return self[attribute_key] end + elsif canonize_name(name) == canonize_name(attribute_key) + return self[attribute_key] end - nil end + nil + end - protected + protected - # stringifies all names so both 'email' and :email return the same result - # @param name [String] The attribute name - # @return [String] stringified name - # - def canonize_name(name) - name.to_s - end + # stringifies all names so both 'email' and :email return the same result + # @param name [String] The attribute name + # @return [String] stringified name + # + def canonize_name(name) + name.to_s end end end diff --git a/lib/onelogin/ruby-saml/authrequest.rb b/lib/onelogin/ruby-saml/authrequest.rb index 3554f2896..9378c1537 100644 --- a/lib/onelogin/ruby-saml/authrequest.rb +++ b/lib/onelogin/ruby-saml/authrequest.rb @@ -8,189 +8,187 @@ require "onelogin/ruby-saml/setting_error" # Only supports SAML 2.0 -module OneLogin - module RubySaml +module RubySaml include REXML - # SAML2 Authentication. AuthNRequest (SSO SP initiated, Builder) + # SAML2 Authentication. AuthNRequest (SSO SP initiated, Builder) + # + class Authrequest < SamlMessage + + # AuthNRequest ID + attr_accessor :uuid + + # Initializes the AuthNRequest. An Authrequest Object that is an extension of the SamlMessage class. + # Asigns an ID, a random uuid. # - class Authrequest < SamlMessage + def initialize + @uuid = RubySaml::Utils.uuid + super() + end - # AuthNRequest ID - attr_accessor :uuid + def request_id + @uuid + end - # Initializes the AuthNRequest. An Authrequest Object that is an extension of the SamlMessage class. - # Asigns an ID, a random uuid. - # - def initialize - @uuid = OneLogin::RubySaml::Utils.uuid - super() + # Creates the AuthNRequest string. + # @param settings [RubySaml::Settings|nil] Toolkit settings + # @param params [Hash] Some extra parameters to be added in the GET for example the RelayState + # @return [String] AuthNRequest string that includes the SAMLRequest + # + def create(settings, params = {}) + params = create_params(settings, params) + params_prefix = /\?/.match?(settings.idp_sso_service_url) ? '&' : '?' + saml_request = CGI.escape(params.delete("SAMLRequest")) + request_params = +"#{params_prefix}SAMLRequest=#{saml_request}" + params.each_pair do |key, value| + request_params << "&#{key}=#{CGI.escape(value.to_s)}" end + raise SettingError.new "Invalid settings, idp_sso_service_url is not set!" if settings.idp_sso_service_url.nil? or settings.idp_sso_service_url.empty? + @login_url = settings.idp_sso_service_url + request_params + end - def request_id - @uuid + # Creates the Get parameters for the request. + # @param settings [RubySaml::Settings|nil] Toolkit settings + # @param params [Hash] Some extra parameters to be added in the GET for example the RelayState + # @return [Hash] Parameters + # + def create_params(settings, params={}) + # The method expects :RelayState but sometimes we get 'RelayState' instead. + # Based on the HashWithIndifferentAccess value in Rails we could experience + # conflicts so this line will solve them. + relay_state = params[:RelayState] || params['RelayState'] + + if relay_state.nil? + params.delete(:RelayState) + params.delete('RelayState') end - # Creates the AuthNRequest string. - # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings - # @param params [Hash] Some extra parameters to be added in the GET for example the RelayState - # @return [String] AuthNRequest string that includes the SAMLRequest - # - def create(settings, params = {}) - params = create_params(settings, params) - params_prefix = /\?/.match?(settings.idp_sso_service_url) ? '&' : '?' - saml_request = CGI.escape(params.delete("SAMLRequest")) - request_params = +"#{params_prefix}SAMLRequest=#{saml_request}" - params.each_pair do |key, value| - request_params << "&#{key}=#{CGI.escape(value.to_s)}" - end - raise SettingError.new "Invalid settings, idp_sso_service_url is not set!" if settings.idp_sso_service_url.nil? or settings.idp_sso_service_url.empty? - @login_url = settings.idp_sso_service_url + request_params + request_doc = create_authentication_xml_doc(settings) + request_doc.context[:attribute_quote] = :quote if settings.double_quote_xml_attribute_values + + request = +"" + request_doc.write(request) + + Logging.debug "Created AuthnRequest: #{request}" + + request = deflate(request) if settings.compress_request + base64_request = encode(request) + request_params = {"SAMLRequest" => base64_request} + sp_signing_key = settings.get_sp_signing_key + + if settings.idp_sso_service_binding == Utils::BINDINGS[:redirect] && settings.security[:authn_requests_signed] && sp_signing_key + params['SigAlg'] = settings.security[:signature_method] + url_string = RubySaml::Utils.build_query( + type: 'SAMLRequest', + data: base64_request, + relay_state: relay_state, + sig_alg: params['SigAlg'] + ) + sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method]) + signature = sp_signing_key.sign(sign_algorithm.new, url_string) + params['Signature'] = encode(signature) end - # Creates the Get parameters for the request. - # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings - # @param params [Hash] Some extra parameters to be added in the GET for example the RelayState - # @return [Hash] Parameters - # - def create_params(settings, params={}) - # The method expects :RelayState but sometimes we get 'RelayState' instead. - # Based on the HashWithIndifferentAccess value in Rails we could experience - # conflicts so this line will solve them. - relay_state = params[:RelayState] || params['RelayState'] - - if relay_state.nil? - params.delete(:RelayState) - params.delete('RelayState') - end + params.each_pair do |key, value| + request_params[key] = value.to_s + end - request_doc = create_authentication_xml_doc(settings) - request_doc.context[:attribute_quote] = :quote if settings.double_quote_xml_attribute_values - - request = +"" - request_doc.write(request) - - Logging.debug "Created AuthnRequest: #{request}" - - request = deflate(request) if settings.compress_request - base64_request = encode(request) - request_params = {"SAMLRequest" => base64_request} - sp_signing_key = settings.get_sp_signing_key - - if settings.idp_sso_service_binding == Utils::BINDINGS[:redirect] && settings.security[:authn_requests_signed] && sp_signing_key - params['SigAlg'] = settings.security[:signature_method] - url_string = OneLogin::RubySaml::Utils.build_query( - type: 'SAMLRequest', - data: base64_request, - relay_state: relay_state, - sig_alg: params['SigAlg'] - ) - sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method]) - signature = sp_signing_key.sign(sign_algorithm.new, url_string) - params['Signature'] = encode(signature) - end + request_params + end - params.each_pair do |key, value| - request_params[key] = value.to_s - end + # Creates the SAMLRequest String. + # @param settings [RubySaml::Settings|nil] Toolkit settings + # @return [String] The SAMLRequest String. + # + def create_authentication_xml_doc(settings) + document = create_xml_document(settings) + sign_document(document, settings) + end - request_params + def create_xml_document(settings) + time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ") + + request_doc = XMLSecurity::Document.new + request_doc.uuid = uuid + + root = request_doc.add_element "samlp:AuthnRequest", { "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol", "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion" } + root.attributes['ID'] = uuid + root.attributes['IssueInstant'] = time + root.attributes['Version'] = "2.0" + root.attributes['Destination'] = settings.idp_sso_service_url unless settings.idp_sso_service_url.nil? or settings.idp_sso_service_url.empty? + root.attributes['IsPassive'] = settings.passive unless settings.passive.nil? + root.attributes['ProtocolBinding'] = settings.protocol_binding unless settings.protocol_binding.nil? + root.attributes["AttributeConsumingServiceIndex"] = settings.attributes_index unless settings.attributes_index.nil? + root.attributes['ForceAuthn'] = settings.force_authn unless settings.force_authn.nil? + + # Conditionally defined elements based on settings + unless settings.assertion_consumer_service_url.nil? + root.attributes["AssertionConsumerServiceURL"] = settings.assertion_consumer_service_url end - # Creates the SAMLRequest String. - # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings - # @return [String] The SAMLRequest String. - # - def create_authentication_xml_doc(settings) - document = create_xml_document(settings) - sign_document(document, settings) + unless settings.sp_entity_id.nil? + issuer = root.add_element "saml:Issuer" + issuer.text = settings.sp_entity_id end - def create_xml_document(settings) - time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ") - - request_doc = XMLSecurity::Document.new - request_doc.uuid = uuid - - root = request_doc.add_element "samlp:AuthnRequest", { "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol", "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion" } - root.attributes['ID'] = uuid - root.attributes['IssueInstant'] = time - root.attributes['Version'] = "2.0" - root.attributes['Destination'] = settings.idp_sso_service_url unless settings.idp_sso_service_url.nil? or settings.idp_sso_service_url.empty? - root.attributes['IsPassive'] = settings.passive unless settings.passive.nil? - root.attributes['ProtocolBinding'] = settings.protocol_binding unless settings.protocol_binding.nil? - root.attributes["AttributeConsumingServiceIndex"] = settings.attributes_index unless settings.attributes_index.nil? - root.attributes['ForceAuthn'] = settings.force_authn unless settings.force_authn.nil? - - # Conditionally defined elements based on settings - unless settings.assertion_consumer_service_url.nil? - root.attributes["AssertionConsumerServiceURL"] = settings.assertion_consumer_service_url - end + unless settings.name_identifier_value_requested.nil? + subject = root.add_element "saml:Subject" - unless settings.sp_entity_id.nil? - issuer = root.add_element "saml:Issuer" - issuer.text = settings.sp_entity_id - end + nameid = subject.add_element "saml:NameID" + nameid.attributes['Format'] = settings.name_identifier_format if settings.name_identifier_format + nameid.text = settings.name_identifier_value_requested - unless settings.name_identifier_value_requested.nil? - subject = root.add_element "saml:Subject" + subject_confirmation = subject.add_element "saml:SubjectConfirmation" + subject_confirmation.attributes['Method'] = "urn:oasis:names:tc:SAML:2.0:cm:bearer" + end - nameid = subject.add_element "saml:NameID" - nameid.attributes['Format'] = settings.name_identifier_format if settings.name_identifier_format - nameid.text = settings.name_identifier_value_requested + unless settings.name_identifier_format.nil? + root.add_element "samlp:NameIDPolicy", { + # Might want to make AllowCreate a setting? + "AllowCreate" => "true", + "Format" => settings.name_identifier_format + } + end - subject_confirmation = subject.add_element "saml:SubjectConfirmation" - subject_confirmation.attributes['Method'] = "urn:oasis:names:tc:SAML:2.0:cm:bearer" - end + if settings.authn_context || settings.authn_context_decl_ref - unless settings.name_identifier_format.nil? - root.add_element "samlp:NameIDPolicy", { - # Might want to make AllowCreate a setting? - "AllowCreate" => "true", - "Format" => settings.name_identifier_format - } + if settings.authn_context_comparison.nil? + comparison = 'exact' + else + comparison = settings.authn_context_comparison end - if settings.authn_context || settings.authn_context_decl_ref - - if settings.authn_context_comparison.nil? - comparison = 'exact' - else - comparison = settings.authn_context_comparison - end - - requested_context = root.add_element "samlp:RequestedAuthnContext", { - "Comparison" => comparison - } + requested_context = root.add_element "samlp:RequestedAuthnContext", { + "Comparison" => comparison + } - unless settings.authn_context.nil? - authn_contexts_class_ref = settings.authn_context.is_a?(Array) ? settings.authn_context : [settings.authn_context] - authn_contexts_class_ref.each do |authn_context_class_ref| - class_ref = requested_context.add_element "saml:AuthnContextClassRef" - class_ref.text = authn_context_class_ref - end + unless settings.authn_context.nil? + authn_contexts_class_ref = settings.authn_context.is_a?(Array) ? settings.authn_context : [settings.authn_context] + authn_contexts_class_ref.each do |authn_context_class_ref| + class_ref = requested_context.add_element "saml:AuthnContextClassRef" + class_ref.text = authn_context_class_ref end + end - unless settings.authn_context_decl_ref.nil? - authn_contexts_decl_refs = settings.authn_context_decl_ref.is_a?(Array) ? settings.authn_context_decl_ref : [settings.authn_context_decl_ref] - authn_contexts_decl_refs.each do |authn_context_decl_ref| - decl_ref = requested_context.add_element "saml:AuthnContextDeclRef" - decl_ref.text = authn_context_decl_ref - end + unless settings.authn_context_decl_ref.nil? + authn_contexts_decl_refs = settings.authn_context_decl_ref.is_a?(Array) ? settings.authn_context_decl_ref : [settings.authn_context_decl_ref] + authn_contexts_decl_refs.each do |authn_context_decl_ref| + decl_ref = requested_context.add_element "saml:AuthnContextDeclRef" + decl_ref.text = authn_context_decl_ref end end - - request_doc end - def sign_document(document, settings) - cert, private_key = settings.get_sp_signing_pair - if settings.idp_sso_service_binding == Utils::BINDINGS[:post] && settings.security[:authn_requests_signed] && private_key && cert - document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method]) - end + request_doc + end - document + def sign_document(document, settings) + cert, private_key = settings.get_sp_signing_pair + if settings.idp_sso_service_binding == Utils::BINDINGS[:post] && settings.security[:authn_requests_signed] && private_key && cert + document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method]) end + + document end end end diff --git a/lib/onelogin/ruby-saml/error_handling.rb b/lib/onelogin/ruby-saml/error_handling.rb index 1222dc309..e2fea9373 100644 --- a/lib/onelogin/ruby-saml/error_handling.rb +++ b/lib/onelogin/ruby-saml/error_handling.rb @@ -2,28 +2,26 @@ require "onelogin/ruby-saml/validation_error" -module OneLogin - module RubySaml - module ErrorHandling - attr_accessor :errors +module RubySaml + module ErrorHandling + attr_accessor :errors - # Append the cause to the errors array, and based on the value of soft, return false or raise - # an exception. soft_override is provided as a means of overriding the object's notion of - # soft for just this invocation. - def append_error(error_msg, soft_override = nil) - @errors << error_msg + # Append the cause to the errors array, and based on the value of soft, return false or raise + # an exception. soft_override is provided as a means of overriding the object's notion of + # soft for just this invocation. + def append_error(error_msg, soft_override = nil) + @errors << error_msg - unless soft_override.nil? ? soft : soft_override - raise ValidationError.new(error_msg) - end - - false + unless soft_override.nil? ? soft : soft_override + raise ValidationError.new(error_msg) end - # Reset the errors array - def reset_errors! - @errors = [] - end + false + end + + # Reset the errors array + def reset_errors! + @errors = [] end end end diff --git a/lib/onelogin/ruby-saml/http_error.rb b/lib/onelogin/ruby-saml/http_error.rb index c3f49a2e4..20f483355 100644 --- a/lib/onelogin/ruby-saml/http_error.rb +++ b/lib/onelogin/ruby-saml/http_error.rb @@ -1,9 +1,6 @@ # frozen_string_literal: true -module OneLogin - module RubySaml - class HttpError < StandardError - end +module RubySaml + class HttpError < StandardError end end - diff --git a/lib/onelogin/ruby-saml/idp_metadata_parser.rb b/lib/onelogin/ruby-saml/idp_metadata_parser.rb index 054109734..44215ad8f 100644 --- a/lib/onelogin/ruby-saml/idp_metadata_parser.rb +++ b/lib/onelogin/ruby-saml/idp_metadata_parser.rb @@ -7,457 +7,455 @@ require "rexml/xpath" # Only supports SAML 2.0 -module OneLogin - module RubySaml - include REXML +module RubySaml + include REXML + + # Auxiliary class to retrieve and parse the Identity Provider Metadata + # + # This class does not validate in any way the URL that is introduced, + # make sure to validate it properly before use it in a parse_remote method. + # Read the `Security warning` section of the README.md file to get more info + # + class IdpMetadataParser + + module SamlMetadata + module Vocabulary + METADATA = "urn:oasis:names:tc:SAML:2.0:metadata" + DSIG = "http://www.w3.org/2000/09/xmldsig#" + NAME_FORMAT = "urn:oasis:names:tc:SAML:2.0:attrname-format:*" + SAML_ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion" + end + + NAMESPACE = { + "md" => Vocabulary::METADATA, + "NameFormat" => Vocabulary::NAME_FORMAT, + "saml" => Vocabulary::SAML_ASSERTION, + "ds" => Vocabulary::DSIG + }.freeze + end + + include SamlMetadata::Vocabulary + attr_reader :document + attr_reader :response + attr_reader :options + + # fetch IdP descriptors from a metadata document + def self.get_idps(metadata_document, only_entity_id=nil) + path = "//md:EntityDescriptor#{"[@entityID=\"#{only_entity_id}\"]" if only_entity_id}/md:IDPSSODescriptor" + REXML::XPath.match( + metadata_document, + path, + SamlMetadata::NAMESPACE + ) + end - # Auxiliary class to retrieve and parse the Identity Provider Metadata + # Parse the Identity Provider metadata and update the settings with the + # IdP values # - # This class does not validate in any way the URL that is introduced, - # make sure to validate it properly before use it in a parse_remote method. - # Read the `Security warning` section of the README.md file to get more info + # @param url [String] Url where the XML of the Identity Provider Metadata is published. + # @param validate_cert [Boolean] If true and the URL is HTTPs, the cert of the domain is checked. # - class IdpMetadataParser - - module SamlMetadata - module Vocabulary - METADATA = "urn:oasis:names:tc:SAML:2.0:metadata" - DSIG = "http://www.w3.org/2000/09/xmldsig#" - NAME_FORMAT = "urn:oasis:names:tc:SAML:2.0:attrname-format:*" - SAML_ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion" - end + # @param options [Hash] options used for parsing the metadata and the returned Settings instance + # @option options [RubySaml::Settings, Hash] :settings the RubySaml::Settings object which gets the parsed metadata merged into or an hash for Settings overrides. + # @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When omitted, the first entity descriptor is used. + # @option options [String, Array, nil] :sso_binding an ordered list of bindings to detect the single signon URL. The first binding in the list that is included in the metadata will be used. + # @option options [String, Array, nil] :slo_binding an ordered list of bindings to detect the single logout URL. The first binding in the list that is included in the metadata will be used. + # @option options [String, Array, nil] :name_id_format an ordered list of NameIDFormats to detect a desired value. The first NameIDFormat in the list that is included in the metadata will be used. + # + # @return [RubySaml::Settings] + # + # @raise [HttpError] Failure to fetch remote IdP metadata + def parse_remote(url, validate_cert = true, options = {}) + idp_metadata = get_idp_metadata(url, validate_cert) + parse(idp_metadata, options) + end - NAMESPACE = { - "md" => Vocabulary::METADATA, - "NameFormat" => Vocabulary::NAME_FORMAT, - "saml" => Vocabulary::SAML_ASSERTION, - "ds" => Vocabulary::DSIG - }.freeze - end + # Parse the Identity Provider metadata and return the results as Hash + # + # @param url [String] Url where the XML of the Identity Provider Metadata is published. + # @param validate_cert [Boolean] If true and the URL is HTTPs, the cert of the domain is checked. + # + # @param options [Hash] options used for parsing the metadata + # @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When omitted, the first entity descriptor is used. + # @option options [String, Array, nil] :sso_binding an ordered list of bindings to detect the single signon URL. The first binding in the list that is included in the metadata will be used. + # @option options [String, Array, nil] :slo_binding an ordered list of bindings to detect the single logout URL. The first binding in the list that is included in the metadata will be used. + # @option options [String, Array, nil] :name_id_format an ordered list of NameIDFormats to detect a desired value. The first NameIDFormat in the list that is included in the metadata will be used. + # + # @return [Hash] + # + # @raise [HttpError] Failure to fetch remote IdP metadata + def parse_remote_to_hash(url, validate_cert = true, options = {}) + parse_remote_to_array(url, validate_cert, options)[0] + end - include SamlMetadata::Vocabulary - attr_reader :document - attr_reader :response - attr_reader :options - - # fetch IdP descriptors from a metadata document - def self.get_idps(metadata_document, only_entity_id=nil) - path = "//md:EntityDescriptor#{"[@entityID=\"#{only_entity_id}\"]" if only_entity_id}/md:IDPSSODescriptor" - REXML::XPath.match( - metadata_document, - path, - SamlMetadata::NAMESPACE - ) - end + # Parse all Identity Provider metadata and return the results as Array + # + # @param url [String] Url where the XML of the Identity Provider Metadata is published. + # @param validate_cert [Boolean] If true and the URL is HTTPs, the cert of the domain is checked. + # + # @param options [Hash] options used for parsing the metadata + # @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When omitted, all found IdPs are returned. + # @option options [String, Array, nil] :sso_binding an ordered list of bindings to detect the single signon URL. The first binding in the list that is included in the metadata will be used. + # @option options [String, Array, nil] :slo_binding an ordered list of bindings to detect the single logout URL. The first binding in the list that is included in the metadata will be used. + # @option options [String, Array, nil] :name_id_format an ordered list of NameIDFormats to detect a desired value. The first NameIDFormat in the list that is included in the metadata will be used. + # + # @return [Array] + # + # @raise [HttpError] Failure to fetch remote IdP metadata + def parse_remote_to_array(url, validate_cert = true, options = {}) + idp_metadata = get_idp_metadata(url, validate_cert) + parse_to_array(idp_metadata, options) + end - # Parse the Identity Provider metadata and update the settings with the - # IdP values - # - # @param url [String] Url where the XML of the Identity Provider Metadata is published. - # @param validate_cert [Boolean] If true and the URL is HTTPs, the cert of the domain is checked. - # - # @param options [Hash] options used for parsing the metadata and the returned Settings instance - # @option options [OneLogin::RubySaml::Settings, Hash] :settings the OneLogin::RubySaml::Settings object which gets the parsed metadata merged into or an hash for Settings overrides. - # @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When omitted, the first entity descriptor is used. - # @option options [String, Array, nil] :sso_binding an ordered list of bindings to detect the single signon URL. The first binding in the list that is included in the metadata will be used. - # @option options [String, Array, nil] :slo_binding an ordered list of bindings to detect the single logout URL. The first binding in the list that is included in the metadata will be used. - # @option options [String, Array, nil] :name_id_format an ordered list of NameIDFormats to detect a desired value. The first NameIDFormat in the list that is included in the metadata will be used. - # - # @return [OneLogin::RubySaml::Settings] - # - # @raise [HttpError] Failure to fetch remote IdP metadata - def parse_remote(url, validate_cert = true, options = {}) - idp_metadata = get_idp_metadata(url, validate_cert) - parse(idp_metadata, options) + # Parse the Identity Provider metadata and update the settings with the IdP values + # + # @param idp_metadata [String] + # + # @param options [Hash] :settings to provide the RubySaml::Settings object or an hash for Settings overrides + # @option options [RubySaml::Settings, Hash] :settings the RubySaml::Settings object which gets the parsed metadata merged into or an hash for Settings overrides. + # @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When omitted, the first entity descriptor is used. + # @option options [String, Array, nil] :sso_binding an ordered list of bindings to detect the single signon URL. The first binding in the list that is included in the metadata will be used. + # @option options [String, Array, nil] :slo_binding an ordered list of bindings to detect the single logout URL. The first binding in the list that is included in the metadata will be used. + # @option options [String, Array, nil] :name_id_format an ordered list of NameIDFormats to detect a desired value. The first NameIDFormat in the list that is included in the metadata will be used. + # + # @return [OneLogin::RubySaml::Settings] + def parse(idp_metadata, options = {}) + parsed_metadata = parse_to_hash(idp_metadata, options) + + unless parsed_metadata[:cache_duration].nil? + cache_valid_until_timestamp = OneLogin::RubySaml::Utils.parse_duration(parsed_metadata[:cache_duration]) + if !cache_valid_until_timestamp.nil? && (parsed_metadata[:valid_until].nil? || cache_valid_until_timestamp < Time.parse(parsed_metadata[:valid_until], Time.now.utc).to_i) + parsed_metadata[:valid_until] = Time.at(cache_valid_until_timestamp).utc.strftime("%Y-%m-%dT%H:%M:%SZ") + end end - - # Parse the Identity Provider metadata and return the results as Hash - # - # @param url [String] Url where the XML of the Identity Provider Metadata is published. - # @param validate_cert [Boolean] If true and the URL is HTTPs, the cert of the domain is checked. - # - # @param options [Hash] options used for parsing the metadata - # @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When omitted, the first entity descriptor is used. - # @option options [String, Array, nil] :sso_binding an ordered list of bindings to detect the single signon URL. The first binding in the list that is included in the metadata will be used. - # @option options [String, Array, nil] :slo_binding an ordered list of bindings to detect the single logout URL. The first binding in the list that is included in the metadata will be used. - # @option options [String, Array, nil] :name_id_format an ordered list of NameIDFormats to detect a desired value. The first NameIDFormat in the list that is included in the metadata will be used. - # - # @return [Hash] - # - # @raise [HttpError] Failure to fetch remote IdP metadata - def parse_remote_to_hash(url, validate_cert = true, options = {}) - parse_remote_to_array(url, validate_cert, options)[0] + # Remove the cache_duration because on the settings + # we only gonna suppot valid_until + parsed_metadata.delete(:cache_duration) + + settings = options[:settings] + + if settings.nil? + OneLogin::RubySaml::Settings.new(parsed_metadata) + elsif settings.is_a?(Hash) + OneLogin::RubySaml::Settings.new(settings.merge(parsed_metadata)) + else + merge_parsed_metadata_into(settings, parsed_metadata) end + end - # Parse all Identity Provider metadata and return the results as Array - # - # @param url [String] Url where the XML of the Identity Provider Metadata is published. - # @param validate_cert [Boolean] If true and the URL is HTTPs, the cert of the domain is checked. - # - # @param options [Hash] options used for parsing the metadata - # @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When omitted, all found IdPs are returned. - # @option options [String, Array, nil] :sso_binding an ordered list of bindings to detect the single signon URL. The first binding in the list that is included in the metadata will be used. - # @option options [String, Array, nil] :slo_binding an ordered list of bindings to detect the single logout URL. The first binding in the list that is included in the metadata will be used. - # @option options [String, Array, nil] :name_id_format an ordered list of NameIDFormats to detect a desired value. The first NameIDFormat in the list that is included in the metadata will be used. - # - # @return [Array] - # - # @raise [HttpError] Failure to fetch remote IdP metadata - def parse_remote_to_array(url, validate_cert = true, options = {}) - idp_metadata = get_idp_metadata(url, validate_cert) - parse_to_array(idp_metadata, options) - end + # Parse the Identity Provider metadata and return the results as Hash + # + # @param idp_metadata [String] + # + # @param options [Hash] options used for parsing the metadata and the returned Settings instance + # @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When omitted, the first entity descriptor is used. + # @option options [String, Array, nil] :sso_binding an ordered list of bindings to detect the single signon URL. The first binding in the list that is included in the metadata will be used. + # @option options [String, Array, nil] :slo_binding an ordered list of bindings to detect the single logout URL. The first binding in the list that is included in the metadata will be used. + # @option options [String, Array, nil] :name_id_format an ordered list of NameIDFormats to detect a desired value. The first NameIDFormat in the list that is included in the metadata will be used. + # + # @return [Hash] + def parse_to_hash(idp_metadata, options = {}) + parse_to_array(idp_metadata, options)[0] + end - # Parse the Identity Provider metadata and update the settings with the IdP values - # - # @param idp_metadata [String] - # - # @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object or an hash for Settings overrides - # @option options [OneLogin::RubySaml::Settings, Hash] :settings the OneLogin::RubySaml::Settings object which gets the parsed metadata merged into or an hash for Settings overrides. - # @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When omitted, the first entity descriptor is used. - # @option options [String, Array, nil] :sso_binding an ordered list of bindings to detect the single signon URL. The first binding in the list that is included in the metadata will be used. - # @option options [String, Array, nil] :slo_binding an ordered list of bindings to detect the single logout URL. The first binding in the list that is included in the metadata will be used. - # @option options [String, Array, nil] :name_id_format an ordered list of NameIDFormats to detect a desired value. The first NameIDFormat in the list that is included in the metadata will be used. - # - # @return [OneLogin::RubySaml::Settings] - def parse(idp_metadata, options = {}) - parsed_metadata = parse_to_hash(idp_metadata, options) - - unless parsed_metadata[:cache_duration].nil? - cache_valid_until_timestamp = OneLogin::RubySaml::Utils.parse_duration(parsed_metadata[:cache_duration]) - if !cache_valid_until_timestamp.nil? && (parsed_metadata[:valid_until].nil? || cache_valid_until_timestamp < Time.parse(parsed_metadata[:valid_until], Time.now.utc).to_i) - parsed_metadata[:valid_until] = Time.at(cache_valid_until_timestamp).utc.strftime("%Y-%m-%dT%H:%M:%SZ") - end - end - # Remove the cache_duration because on the settings - # we only gonna suppot valid_until - parsed_metadata.delete(:cache_duration) + # Parse all Identity Provider metadata and return the results as Array + # + # @param idp_metadata [String] + # + # @param options [Hash] options used for parsing the metadata and the returned Settings instance + # @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When omitted, all found IdPs are returned. + # @option options [String, Array, nil] :sso_binding an ordered list of bindings to detect the single signon URL. The first binding in the list that is included in the metadata will be used. + # @option options [String, Array, nil] :slo_binding an ordered list of bindings to detect the single logout URL. The first binding in the list that is included in the metadata will be used. + # @option options [String, Array, nil] :name_id_format an ordered list of NameIDFormats to detect a desired value. The first NameIDFormat in the list that is included in the metadata will be used. + # + # @return [Array] + def parse_to_array(idp_metadata, options = {}) + parse_to_idp_metadata_array(idp_metadata, options).map { |idp_md| idp_md.to_hash(options) } + end - settings = options[:settings] + def parse_to_idp_metadata_array(idp_metadata, options = {}) + @document = REXML::Document.new(idp_metadata) + @options = options - if settings.nil? - OneLogin::RubySaml::Settings.new(parsed_metadata) - elsif settings.is_a?(Hash) - OneLogin::RubySaml::Settings.new(settings.merge(parsed_metadata)) - else - merge_parsed_metadata_into(settings, parsed_metadata) - end + idpsso_descriptors = self.class.get_idps(@document, options[:entity_id]) + if idpsso_descriptors.none? + raise ArgumentError.new("idp_metadata must contain an IDPSSODescriptor element") end - # Parse the Identity Provider metadata and return the results as Hash - # - # @param idp_metadata [String] - # - # @param options [Hash] options used for parsing the metadata and the returned Settings instance - # @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When omitted, the first entity descriptor is used. - # @option options [String, Array, nil] :sso_binding an ordered list of bindings to detect the single signon URL. The first binding in the list that is included in the metadata will be used. - # @option options [String, Array, nil] :slo_binding an ordered list of bindings to detect the single logout URL. The first binding in the list that is included in the metadata will be used. - # @option options [String, Array, nil] :name_id_format an ordered list of NameIDFormats to detect a desired value. The first NameIDFormat in the list that is included in the metadata will be used. - # - # @return [Hash] - def parse_to_hash(idp_metadata, options = {}) - parse_to_array(idp_metadata, options)[0] - end + idpsso_descriptors.map {|id| IdpMetadata.new(id, id.parent.attributes["entityID"])} + end - # Parse all Identity Provider metadata and return the results as Array - # - # @param idp_metadata [String] - # - # @param options [Hash] options used for parsing the metadata and the returned Settings instance - # @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When omitted, all found IdPs are returned. - # @option options [String, Array, nil] :sso_binding an ordered list of bindings to detect the single signon URL. The first binding in the list that is included in the metadata will be used. - # @option options [String, Array, nil] :slo_binding an ordered list of bindings to detect the single logout URL. The first binding in the list that is included in the metadata will be used. - # @option options [String, Array, nil] :name_id_format an ordered list of NameIDFormats to detect a desired value. The first NameIDFormat in the list that is included in the metadata will be used. - # - # @return [Array] - def parse_to_array(idp_metadata, options = {}) - parse_to_idp_metadata_array(idp_metadata, options).map { |idp_md| idp_md.to_hash(options) } + private + + # Retrieve the remote IdP metadata from the URL or a cached copy. + # @param url [String] Url where the XML of the Identity Provider Metadata is published. + # @param validate_cert [Boolean] If true and the URL is HTTPs, the cert of the domain is checked. + # @return [REXML::document] Parsed XML IdP metadata + # @raise [HttpError] Failure to fetch remote IdP metadata + def get_idp_metadata(url, validate_cert) + uri = URI.parse(url) + raise ArgumentError.new("url must begin with http or https") unless /^https?/.match?(uri.scheme) + http = Net::HTTP.new(uri.host, uri.port) + + if uri.scheme == "https" + http.use_ssl = true + # Most IdPs will probably use self signed certs + http.verify_mode = validate_cert ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE end - def parse_to_idp_metadata_array(idp_metadata, options = {}) - @document = REXML::Document.new(idp_metadata) - @options = options + get = Net::HTTP::Get.new(uri.request_uri) + get.basic_auth uri.user, uri.password if uri.user + @response = http.request(get) + return response.body if response.is_a? Net::HTTPSuccess - idpsso_descriptors = self.class.get_idps(@document, options[:entity_id]) - if idpsso_descriptors.none? - raise ArgumentError.new("idp_metadata must contain an IDPSSODescriptor element") - end + raise OneLogin::RubySaml::HttpError.new( + "Failed to fetch idp metadata: #{response.code}: #{response.message}" + ) + end - idpsso_descriptors.map {|id| IdpMetadata.new(id, id.parent.attributes["entityID"])} - end + class IdpMetadata + attr_reader :idpsso_descriptor, :entity_id - private + def initialize(idpsso_descriptor, entity_id) + @idpsso_descriptor = idpsso_descriptor + @entity_id = entity_id + end - # Retrieve the remote IdP metadata from the URL or a cached copy. - # @param url [String] Url where the XML of the Identity Provider Metadata is published. - # @param validate_cert [Boolean] If true and the URL is HTTPs, the cert of the domain is checked. - # @return [REXML::document] Parsed XML IdP metadata - # @raise [HttpError] Failure to fetch remote IdP metadata - def get_idp_metadata(url, validate_cert) - uri = URI.parse(url) - raise ArgumentError.new("url must begin with http or https") unless /^https?/.match?(uri.scheme) - http = Net::HTTP.new(uri.host, uri.port) - - if uri.scheme == "https" - http.use_ssl = true - # Most IdPs will probably use self signed certs - http.verify_mode = validate_cert ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE + def to_hash(options = {}) + sso_binding = options[:sso_binding] + slo_binding = options[:slo_binding] + { + idp_entity_id: @entity_id, + name_identifier_format: idp_name_id_format(options[:name_id_format]), + idp_sso_service_url: single_signon_service_url(sso_binding), + idp_sso_service_binding: single_signon_service_binding(sso_binding), + idp_slo_service_url: single_logout_service_url(slo_binding), + idp_slo_service_binding: single_logout_service_binding(slo_binding), + idp_slo_response_service_url: single_logout_response_service_url(slo_binding), + idp_attribute_names: attribute_names, + idp_cert: nil, + idp_cert_fingerprint: nil, + idp_cert_multi: nil, + valid_until: valid_until, + cache_duration: cache_duration + }.tap do |response_hash| + merge_certificates_into(response_hash) unless certificates.nil? end + end - get = Net::HTTP::Get.new(uri.request_uri) - get.basic_auth uri.user, uri.password if uri.user - @response = http.request(get) - return response.body if response.is_a? Net::HTTPSuccess + # @return [String|nil] 'validUntil' attribute of metadata + # + def valid_until + root = @idpsso_descriptor.root + root.attributes['validUntil'] if root&.attributes + end - raise OneLogin::RubySaml::HttpError.new( - "Failed to fetch idp metadata: #{response.code}: #{response.message}" - ) + # @return [String|nil] 'cacheDuration' attribute of metadata + # + def cache_duration + root = @idpsso_descriptor.root + root.attributes['cacheDuration'] if root&.attributes end - class IdpMetadata - attr_reader :idpsso_descriptor, :entity_id + # @param name_id_priority [String|Array] The prioritized list of NameIDFormat values to select. Will select first value if nil. + # @return [String|nil] IdP NameIDFormat value if exists + # + def idp_name_id_format(name_id_priority = nil) + nodes = REXML::XPath.match( + @idpsso_descriptor, + "md:NameIDFormat", + SamlMetadata::NAMESPACE + ) + first_ranked_text(nodes, name_id_priority) + end - def initialize(idpsso_descriptor, entity_id) - @idpsso_descriptor = idpsso_descriptor - @entity_id = entity_id - end + # @param binding_priority [String|Array] The prioritized list of Binding values to select. Will select first value if nil. + # @return [String|nil] SingleSignOnService binding if exists + # + def single_signon_service_binding(binding_priority = nil) + nodes = REXML::XPath.match( + @idpsso_descriptor, + "md:SingleSignOnService/@Binding", + SamlMetadata::NAMESPACE + ) + first_ranked_value(nodes, binding_priority) + end - def to_hash(options = {}) - sso_binding = options[:sso_binding] - slo_binding = options[:slo_binding] - { - idp_entity_id: @entity_id, - name_identifier_format: idp_name_id_format(options[:name_id_format]), - idp_sso_service_url: single_signon_service_url(sso_binding), - idp_sso_service_binding: single_signon_service_binding(sso_binding), - idp_slo_service_url: single_logout_service_url(slo_binding), - idp_slo_service_binding: single_logout_service_binding(slo_binding), - idp_slo_response_service_url: single_logout_response_service_url(slo_binding), - idp_attribute_names: attribute_names, - idp_cert: nil, - idp_cert_fingerprint: nil, - idp_cert_multi: nil, - valid_until: valid_until, - cache_duration: cache_duration - }.tap do |response_hash| - merge_certificates_into(response_hash) unless certificates.nil? - end - end + # @param binding_priority [String|Array] The prioritized list of Binding values to select. Will select first value if nil. + # @return [String|nil] SingleLogoutService binding if exists + # + def single_logout_service_binding(binding_priority = nil) + nodes = REXML::XPath.match( + @idpsso_descriptor, + "md:SingleLogoutService/@Binding", + SamlMetadata::NAMESPACE + ) + first_ranked_value(nodes, binding_priority) + end - # @return [String|nil] 'validUntil' attribute of metadata - # - def valid_until - root = @idpsso_descriptor.root - root.attributes['validUntil'] if root&.attributes - end + # @param binding_priority [String|Array] The prioritized list of Binding values to select. Will select first value if nil. + # @return [String|nil] SingleSignOnService endpoint if exists + # + def single_signon_service_url(binding_priority = nil) + binding = single_signon_service_binding(binding_priority) + return if binding.nil? - # @return [String|nil] 'cacheDuration' attribute of metadata - # - def cache_duration - root = @idpsso_descriptor.root - root.attributes['cacheDuration'] if root&.attributes - end + node = REXML::XPath.first( + @idpsso_descriptor, + "md:SingleSignOnService[@Binding=\"#{binding}\"]/@Location", + SamlMetadata::NAMESPACE + ) + node&.value + end - # @param name_id_priority [String|Array] The prioritized list of NameIDFormat values to select. Will select first value if nil. - # @return [String|nil] IdP NameIDFormat value if exists - # - def idp_name_id_format(name_id_priority = nil) - nodes = REXML::XPath.match( - @idpsso_descriptor, - "md:NameIDFormat", - SamlMetadata::NAMESPACE - ) - first_ranked_text(nodes, name_id_priority) - end + # @param binding_priority [String|Array] The prioritized list of Binding values to select. Will select first value if nil. + # @return [String|nil] SingleLogoutService endpoint if exists + # + def single_logout_service_url(binding_priority = nil) + binding = single_logout_service_binding(binding_priority) + return if binding.nil? - # @param binding_priority [String|Array] The prioritized list of Binding values to select. Will select first value if nil. - # @return [String|nil] SingleSignOnService binding if exists - # - def single_signon_service_binding(binding_priority = nil) - nodes = REXML::XPath.match( - @idpsso_descriptor, - "md:SingleSignOnService/@Binding", - SamlMetadata::NAMESPACE - ) - first_ranked_value(nodes, binding_priority) - end + node = REXML::XPath.first( + @idpsso_descriptor, + "md:SingleLogoutService[@Binding=\"#{binding}\"]/@Location", + SamlMetadata::NAMESPACE + ) + node&.value + end - # @param binding_priority [String|Array] The prioritized list of Binding values to select. Will select first value if nil. - # @return [String|nil] SingleLogoutService binding if exists - # - def single_logout_service_binding(binding_priority = nil) - nodes = REXML::XPath.match( - @idpsso_descriptor, - "md:SingleLogoutService/@Binding", - SamlMetadata::NAMESPACE - ) - first_ranked_value(nodes, binding_priority) - end + # @param binding_priority [String|Array] The prioritized list of Binding values to select. Will select first value if nil. + # @return [String|nil] SingleLogoutService response url if exists + # + def single_logout_response_service_url(binding_priority = nil) + binding = single_logout_service_binding(binding_priority) + return if binding.nil? - # @param binding_priority [String|Array] The prioritized list of Binding values to select. Will select first value if nil. - # @return [String|nil] SingleSignOnService endpoint if exists - # - def single_signon_service_url(binding_priority = nil) - binding = single_signon_service_binding(binding_priority) - return if binding.nil? + node = REXML::XPath.first( + @idpsso_descriptor, + "md:SingleLogoutService[@Binding=\"#{binding}\"]/@ResponseLocation", + SamlMetadata::NAMESPACE + ) + node&.value + end - node = REXML::XPath.first( + # @return [String|nil] Unformatted Certificate if exists + # + def certificates + @certificates ||= begin + signing_nodes = REXML::XPath.match( @idpsso_descriptor, - "md:SingleSignOnService[@Binding=\"#{binding}\"]/@Location", + "md:KeyDescriptor[not(contains(@use, 'encryption'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate", SamlMetadata::NAMESPACE ) - node&.value - end - # @param binding_priority [String|Array] The prioritized list of Binding values to select. Will select first value if nil. - # @return [String|nil] SingleLogoutService endpoint if exists - # - def single_logout_service_url(binding_priority = nil) - binding = single_logout_service_binding(binding_priority) - return if binding.nil? - - node = REXML::XPath.first( + encryption_nodes = REXML::XPath.match( @idpsso_descriptor, - "md:SingleLogoutService[@Binding=\"#{binding}\"]/@Location", + "md:KeyDescriptor[not(contains(@use, 'signing'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate", SamlMetadata::NAMESPACE ) - node&.value - end - # @param binding_priority [String|Array] The prioritized list of Binding values to select. Will select first value if nil. - # @return [String|nil] SingleLogoutService response url if exists - # - def single_logout_response_service_url(binding_priority = nil) - binding = single_logout_service_binding(binding_priority) - return if binding.nil? - - node = REXML::XPath.first( - @idpsso_descriptor, - "md:SingleLogoutService[@Binding=\"#{binding}\"]/@ResponseLocation", - SamlMetadata::NAMESPACE - ) - node&.value - end + return nil if signing_nodes.empty? && encryption_nodes.empty? - # @return [String|nil] Unformatted Certificate if exists - # - def certificates - @certificates ||= begin - signing_nodes = REXML::XPath.match( - @idpsso_descriptor, - "md:KeyDescriptor[not(contains(@use, 'encryption'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate", - SamlMetadata::NAMESPACE - ) - - encryption_nodes = REXML::XPath.match( - @idpsso_descriptor, - "md:KeyDescriptor[not(contains(@use, 'signing'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate", - SamlMetadata::NAMESPACE - ) - - return nil if signing_nodes.empty? && encryption_nodes.empty? - - certs = {} - unless signing_nodes.empty? - certs['signing'] = [] - signing_nodes.each do |cert_node| - certs['signing'] << Utils.element_text(cert_node) - end + certs = {} + unless signing_nodes.empty? + certs['signing'] = [] + signing_nodes.each do |cert_node| + certs['signing'] << Utils.element_text(cert_node) end + end - unless encryption_nodes.empty? - certs['encryption'] = [] - encryption_nodes.each do |cert_node| - certs['encryption'] << Utils.element_text(cert_node) - end + unless encryption_nodes.empty? + certs['encryption'] = [] + encryption_nodes.each do |cert_node| + certs['encryption'] << Utils.element_text(cert_node) end - certs end + certs end + end - # @return [String|nil] the fingerpint of the X509Certificate if it exists - # - def fingerprint(certificate, fingerprint_algorithm = XMLSecurity::Document::SHA1) - @fingerprint ||= begin - return unless certificate + # @return [String|nil] the fingerpint of the X509Certificate if it exists + # + def fingerprint(certificate, fingerprint_algorithm = XMLSecurity::Document::SHA1) + @fingerprint ||= begin + return unless certificate - cert = OpenSSL::X509::Certificate.new(Base64.decode64(certificate)) + cert = OpenSSL::X509::Certificate.new(Base64.decode64(certificate)) - fingerprint_alg = XMLSecurity::BaseDocument.new.algorithm(fingerprint_algorithm).new - fingerprint_alg.hexdigest(cert.to_der).upcase.scan(/../).join(":") - end + fingerprint_alg = XMLSecurity::BaseDocument.new.algorithm(fingerprint_algorithm).new + fingerprint_alg.hexdigest(cert.to_der).upcase.scan(/../).join(":") end + end - # @return [Array] the names of all SAML attributes if any exist - # - def attribute_names - nodes = REXML::XPath.match( - @idpsso_descriptor, - "saml:Attribute/@Name", - SamlMetadata::NAMESPACE + # @return [Array] the names of all SAML attributes if any exist + # + def attribute_names + nodes = REXML::XPath.match( + @idpsso_descriptor, + "saml:Attribute/@Name", + SamlMetadata::NAMESPACE + ) + nodes.map(&:value) + end + + def merge_certificates_into(parsed_metadata) + if (certificates.size == 1 && + (certificates_has_one('signing') || certificates_has_one('encryption'))) || + (certificates_has_one('signing') && certificates_has_one('encryption') && + certificates["signing"][0] == certificates["encryption"][0]) + + parsed_metadata[:idp_cert] = if certificates.key?("signing") + certificates["signing"][0] + else + certificates["encryption"][0] + end + parsed_metadata[:idp_cert_fingerprint] = fingerprint( + parsed_metadata[:idp_cert], + parsed_metadata[:idp_cert_fingerprint_algorithm] ) - nodes.map(&:value) end - def merge_certificates_into(parsed_metadata) - if (certificates.size == 1 && - (certificates_has_one('signing') || certificates_has_one('encryption'))) || - (certificates_has_one('signing') && certificates_has_one('encryption') && - certificates["signing"][0] == certificates["encryption"][0]) - - parsed_metadata[:idp_cert] = if certificates.key?("signing") - certificates["signing"][0] - else - certificates["encryption"][0] - end - parsed_metadata[:idp_cert_fingerprint] = fingerprint( - parsed_metadata[:idp_cert], - parsed_metadata[:idp_cert_fingerprint_algorithm] - ) - end - - # symbolize keys of certificates and pass it on - parsed_metadata[:idp_cert_multi] = certificates.transform_keys(&:to_sym) - end + # symbolize keys of certificates and pass it on + parsed_metadata[:idp_cert_multi] = certificates.transform_keys(&:to_sym) + end - def certificates_has_one(key) - certificates.key?(key) && certificates[key].size == 1 - end + def certificates_has_one(key) + certificates.key?(key) && certificates[key].size == 1 + end - private + private - def first_ranked_text(nodes, priority = nil) - return unless nodes.any? + def first_ranked_text(nodes, priority = nil) + return unless nodes.any? - priority = Array(priority) - if priority.any? - values = nodes.map(&:text) - priority.detect { |candidate| values.include?(candidate) } - else - nodes.first.text - end + priority = Array(priority) + if priority.any? + values = nodes.map(&:text) + priority.detect { |candidate| values.include?(candidate) } + else + nodes.first.text end + end - def first_ranked_value(nodes, priority = nil) - return unless nodes.any? + def first_ranked_value(nodes, priority = nil) + return unless nodes.any? - priority = Array(priority) - if priority.any? - values = nodes.map(&:value) - priority.detect { |candidate| values.include?(candidate) } - else - nodes.first.value - end + priority = Array(priority) + if priority.any? + values = nodes.map(&:value) + priority.detect { |candidate| values.include?(candidate) } + else + nodes.first.value end end + end - def merge_parsed_metadata_into(settings, parsed_metadata) - parsed_metadata.each do |key, value| - settings.send("#{key}=".to_sym, value) - end - - settings + def merge_parsed_metadata_into(settings, parsed_metadata) + parsed_metadata.each do |key, value| + settings.send("#{key}=".to_sym, value) end + + settings end end end diff --git a/lib/onelogin/ruby-saml/logging.rb b/lib/onelogin/ruby-saml/logging.rb index 9915a0d8a..af612c1dd 100644 --- a/lib/onelogin/ruby-saml/logging.rb +++ b/lib/onelogin/ruby-saml/logging.rb @@ -3,33 +3,31 @@ require 'logger' # Simplistic log class when we're running in Rails -module OneLogin - module RubySaml - class Logging - DEFAULT_LOGGER = ::Logger.new($stdout) - - def self.logger - @logger ||= begin - logger = Rails.logger if defined?(::Rails) && Rails.respond_to?(:logger) - logger ||= DEFAULT_LOGGER - end +module RubySaml + class Logging + DEFAULT_LOGGER = ::Logger.new($stdout) + + def self.logger + @logger ||= begin + logger = Rails.logger if defined?(::Rails) && Rails.respond_to?(:logger) + logger ||= DEFAULT_LOGGER end + end - class << self - attr_writer :logger - end + class << self + attr_writer :logger + end - def self.debug(message) - return if ENV["ruby-saml/testing"] + def self.debug(message) + return if ENV["ruby-saml/testing"] - logger.debug(message) - end + logger.debug(message) + end - def self.info(message) - return if ENV["ruby-saml/testing"] + def self.info(message) + return if ENV["ruby-saml/testing"] - logger.info(message) - end + logger.info(message) end end end diff --git a/lib/onelogin/ruby-saml/logoutrequest.rb b/lib/onelogin/ruby-saml/logoutrequest.rb index 9b5d083bd..bba92f27d 100644 --- a/lib/onelogin/ruby-saml/logoutrequest.rb +++ b/lib/onelogin/ruby-saml/logoutrequest.rb @@ -6,149 +6,147 @@ require "onelogin/ruby-saml/setting_error" # Only supports SAML 2.0 -module OneLogin - module RubySaml +module RubySaml - # SAML2 Logout Request (SLO SP initiated, Builder) + # SAML2 Logout Request (SLO SP initiated, Builder) + # + class Logoutrequest < SamlMessage + + # Logout Request ID + attr_accessor :uuid + + # Initializes the Logout Request. A Logoutrequest Object that is an extension of the SamlMessage class. + # Asigns an ID, a random uuid. # - class Logoutrequest < SamlMessage + def initialize + @uuid = RubySaml::Utils.uuid + super() + end - # Logout Request ID - attr_accessor :uuid + def request_id + @uuid + end - # Initializes the Logout Request. A Logoutrequest Object that is an extension of the SamlMessage class. - # Asigns an ID, a random uuid. - # - def initialize - @uuid = OneLogin::RubySaml::Utils.uuid - super() + # Creates the Logout Request string. + # @param settings [RubySaml::Settings|nil] Toolkit settings + # @param params [Hash] Some extra parameters to be added in the GET for example the RelayState + # @return [String] Logout Request string that includes the SAMLRequest + # + def create(settings, params={}) + params = create_params(settings, params) + params_prefix = /\?/.match?(settings.idp_slo_service_url) ? '&' : '?' + saml_request = CGI.escape(params.delete("SAMLRequest")) + request_params = +"#{params_prefix}SAMLRequest=#{saml_request}" + params.each_pair do |key, value| + request_params << "&#{key}=#{CGI.escape(value.to_s)}" end + raise SettingError.new "Invalid settings, idp_slo_service_url is not set!" if settings.idp_slo_service_url.nil? or settings.idp_slo_service_url.empty? + @logout_url = settings.idp_slo_service_url + request_params + end - def request_id - @uuid + # Creates the Get parameters for the logout request. + # @param settings [RubySaml::Settings|nil] Toolkit settings + # @param params [Hash] Some extra parameters to be added in the GET for example the RelayState + # @return [Hash] Parameters + # + def create_params(settings, params={}) + # The method expects :RelayState but sometimes we get 'RelayState' instead. + # Based on the HashWithIndifferentAccess value in Rails we could experience + # conflicts so this line will solve them. + relay_state = params[:RelayState] || params['RelayState'] + + if relay_state.nil? + params.delete(:RelayState) + params.delete('RelayState') end - # Creates the Logout Request string. - # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings - # @param params [Hash] Some extra parameters to be added in the GET for example the RelayState - # @return [String] Logout Request string that includes the SAMLRequest - # - def create(settings, params={}) - params = create_params(settings, params) - params_prefix = /\?/.match?(settings.idp_slo_service_url) ? '&' : '?' - saml_request = CGI.escape(params.delete("SAMLRequest")) - request_params = +"#{params_prefix}SAMLRequest=#{saml_request}" - params.each_pair do |key, value| - request_params << "&#{key}=#{CGI.escape(value.to_s)}" - end - raise SettingError.new "Invalid settings, idp_slo_service_url is not set!" if settings.idp_slo_service_url.nil? or settings.idp_slo_service_url.empty? - @logout_url = settings.idp_slo_service_url + request_params + request_doc = create_logout_request_xml_doc(settings) + request_doc.context[:attribute_quote] = :quote if settings.double_quote_xml_attribute_values + + request = +"" + request_doc.write(request) + + Logging.debug "Created SLO Logout Request: #{request}" + + request = deflate(request) if settings.compress_request + base64_request = encode(request) + request_params = {"SAMLRequest" => base64_request} + sp_signing_key = settings.get_sp_signing_key + + if settings.idp_slo_service_binding == Utils::BINDINGS[:redirect] && settings.security[:logout_requests_signed] && sp_signing_key + params['SigAlg'] = settings.security[:signature_method] + url_string = RubySaml::Utils.build_query( + type: 'SAMLRequest', + data: base64_request, + relay_state: relay_state, + sig_alg: params['SigAlg'] + ) + sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method]) + signature = settings.get_sp_signing_key.sign(sign_algorithm.new, url_string) + params['Signature'] = encode(signature) end - # Creates the Get parameters for the logout request. - # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings - # @param params [Hash] Some extra parameters to be added in the GET for example the RelayState - # @return [Hash] Parameters - # - def create_params(settings, params={}) - # The method expects :RelayState but sometimes we get 'RelayState' instead. - # Based on the HashWithIndifferentAccess value in Rails we could experience - # conflicts so this line will solve them. - relay_state = params[:RelayState] || params['RelayState'] - - if relay_state.nil? - params.delete(:RelayState) - params.delete('RelayState') - end - - request_doc = create_logout_request_xml_doc(settings) - request_doc.context[:attribute_quote] = :quote if settings.double_quote_xml_attribute_values - - request = +"" - request_doc.write(request) - - Logging.debug "Created SLO Logout Request: #{request}" - - request = deflate(request) if settings.compress_request - base64_request = encode(request) - request_params = {"SAMLRequest" => base64_request} - sp_signing_key = settings.get_sp_signing_key - - if settings.idp_slo_service_binding == Utils::BINDINGS[:redirect] && settings.security[:logout_requests_signed] && sp_signing_key - params['SigAlg'] = settings.security[:signature_method] - url_string = OneLogin::RubySaml::Utils.build_query( - type: 'SAMLRequest', - data: base64_request, - relay_state: relay_state, - sig_alg: params['SigAlg'] - ) - sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method]) - signature = settings.get_sp_signing_key.sign(sign_algorithm.new, url_string) - params['Signature'] = encode(signature) - end - - params.each_pair do |key, value| - request_params[key] = value.to_s - end - - request_params + params.each_pair do |key, value| + request_params[key] = value.to_s end - # Creates the SAMLRequest String. - # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings - # @return [String] The SAMLRequest String. - # - def create_logout_request_xml_doc(settings) - document = create_xml_document(settings) - sign_document(document, settings) + request_params + end + + # Creates the SAMLRequest String. + # @param settings [RubySaml::Settings|nil] Toolkit settings + # @return [String] The SAMLRequest String. + # + def create_logout_request_xml_doc(settings) + document = create_xml_document(settings) + sign_document(document, settings) + end + + def create_xml_document(settings) + time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ") + + request_doc = XMLSecurity::Document.new + request_doc.uuid = uuid + + root = request_doc.add_element "samlp:LogoutRequest", { "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol", "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion" } + root.attributes['ID'] = uuid + root.attributes['IssueInstant'] = time + root.attributes['Version'] = "2.0" + root.attributes['Destination'] = settings.idp_slo_service_url unless settings.idp_slo_service_url.nil? or settings.idp_slo_service_url.empty? + + if settings.sp_entity_id + issuer = root.add_element "saml:Issuer" + issuer.text = settings.sp_entity_id end - def create_xml_document(settings) - time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ") - - request_doc = XMLSecurity::Document.new - request_doc.uuid = uuid - - root = request_doc.add_element "samlp:LogoutRequest", { "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol", "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion" } - root.attributes['ID'] = uuid - root.attributes['IssueInstant'] = time - root.attributes['Version'] = "2.0" - root.attributes['Destination'] = settings.idp_slo_service_url unless settings.idp_slo_service_url.nil? or settings.idp_slo_service_url.empty? - - if settings.sp_entity_id - issuer = root.add_element "saml:Issuer" - issuer.text = settings.sp_entity_id - end - - nameid = root.add_element "saml:NameID" - if settings.name_identifier_value - nameid.attributes['NameQualifier'] = settings.idp_name_qualifier if settings.idp_name_qualifier - nameid.attributes['SPNameQualifier'] = settings.sp_name_qualifier if settings.sp_name_qualifier - nameid.attributes['Format'] = settings.name_identifier_format if settings.name_identifier_format - nameid.text = settings.name_identifier_value - else - # If no NameID is present in the settings we generate one - nameid.text = OneLogin::RubySaml::Utils.uuid - nameid.attributes['Format'] = 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' - end - - if settings.sessionindex - sessionindex = root.add_element "samlp:SessionIndex" - sessionindex.text = settings.sessionindex - end - - request_doc + nameid = root.add_element "saml:NameID" + if settings.name_identifier_value + nameid.attributes['NameQualifier'] = settings.idp_name_qualifier if settings.idp_name_qualifier + nameid.attributes['SPNameQualifier'] = settings.sp_name_qualifier if settings.sp_name_qualifier + nameid.attributes['Format'] = settings.name_identifier_format if settings.name_identifier_format + nameid.text = settings.name_identifier_value + else + # If no NameID is present in the settings we generate one + nameid.text = RubySaml::Utils.uuid + nameid.attributes['Format'] = 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' end - def sign_document(document, settings) - # embed signature - cert, private_key = settings.get_sp_signing_pair - if settings.idp_slo_service_binding == Utils::BINDINGS[:post] && settings.security[:logout_requests_signed] && private_key && cert - document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method]) - end + if settings.sessionindex + sessionindex = root.add_element "samlp:SessionIndex" + sessionindex.text = settings.sessionindex + end + + request_doc + end - document + def sign_document(document, settings) + # embed signature + cert, private_key = settings.get_sp_signing_pair + if settings.idp_slo_service_binding == Utils::BINDINGS[:post] && settings.security[:logout_requests_signed] && private_key && cert + document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method]) end + + document end end end diff --git a/lib/onelogin/ruby-saml/logoutresponse.rb b/lib/onelogin/ruby-saml/logoutresponse.rb index 5b85d2fcc..b70cb16c9 100644 --- a/lib/onelogin/ruby-saml/logoutresponse.rb +++ b/lib/onelogin/ruby-saml/logoutresponse.rb @@ -6,274 +6,272 @@ require "time" # Only supports SAML 2.0 -module OneLogin - module RubySaml +module RubySaml - # SAML2 Logout Response (SLO IdP initiated, Parser) - # - class Logoutresponse < SamlMessage - include ErrorHandling - - # OneLogin::RubySaml::Settings Toolkit settings - attr_accessor :settings - - attr_reader :document - attr_reader :response - attr_reader :options - - attr_accessor :soft - - # Constructs the Logout Response. A Logout Response Object that is an extension of the SamlMessage class. - # @param response [String] A UUEncoded logout response from the IdP. - # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings - # @param options [Hash] Extra parameters. - # :matches_request_id It will validate that the logout response matches the ID of the request. - # :get_params GET Parameters, including the SAMLResponse - # :relax_signature_validation to accept signatures if no idp certificate registered on settings - # - # @raise [ArgumentError] if response is nil - # - def initialize(response, settings = nil, options = {}) - @errors = [] - raise ArgumentError.new("Logoutresponse cannot be nil") if response.nil? - @settings = settings - - if settings.nil? || settings.soft.nil? - @soft = true - else - @soft = settings.soft - end + # SAML2 Logout Response (SLO IdP initiated, Parser) + # + class Logoutresponse < SamlMessage + include ErrorHandling - @options = options - @response = decode_raw_saml(response, settings) - @document = XMLSecurity::SignedDocument.new(@response) - super() - end + # RubySaml::Settings Toolkit settings + attr_accessor :settings - def response_id - id(document) - end + attr_reader :document + attr_reader :response + attr_reader :options - # Checks if the Status has the "Success" code - # @return [Boolean] True if the StatusCode is Sucess - # @raise [ValidationError] if soft == false and validation fails - # - def success? - status_code == "urn:oasis:names:tc:SAML:2.0:status:Success" - end + attr_accessor :soft - # @return [String|nil] Gets the InResponseTo attribute from the Logout Response if exists. - # - def in_response_to - @in_response_to ||= begin - node = REXML::XPath.first( - document, - "/p:LogoutResponse", - { "p" => PROTOCOL } - ) - node.nil? ? nil : node.attributes['InResponseTo'] - end + # Constructs the Logout Response. A Logout Response Object that is an extension of the SamlMessage class. + # @param response [String] A UUEncoded logout response from the IdP. + # @param settings [RubySaml::Settings|nil] Toolkit settings + # @param options [Hash] Extra parameters. + # :matches_request_id It will validate that the logout response matches the ID of the request. + # :get_params GET Parameters, including the SAMLResponse + # :relax_signature_validation to accept signatures if no idp certificate registered on settings + # + # @raise [ArgumentError] if response is nil + # + def initialize(response, settings = nil, options = {}) + @errors = [] + raise ArgumentError.new("Logoutresponse cannot be nil") if response.nil? + @settings = settings + + if settings.nil? || settings.soft.nil? + @soft = true + else + @soft = settings.soft end - # @return [String] Gets the Issuer from the Logout Response. - # - def issuer - @issuer ||= begin - node = REXML::XPath.first( - document, - "/p:LogoutResponse/a:Issuer", - { "p" => PROTOCOL, "a" => ASSERTION } - ) - Utils.element_text(node) - end + @options = options + @response = decode_raw_saml(response, settings) + @document = XMLSecurity::SignedDocument.new(@response) + super() + end + + def response_id + id(document) + end + + # Checks if the Status has the "Success" code + # @return [Boolean] True if the StatusCode is Sucess + # @raise [ValidationError] if soft == false and validation fails + # + def success? + status_code == "urn:oasis:names:tc:SAML:2.0:status:Success" + end + + # @return [String|nil] Gets the InResponseTo attribute from the Logout Response if exists. + # + def in_response_to + @in_response_to ||= begin + node = REXML::XPath.first( + document, + "/p:LogoutResponse", + { "p" => PROTOCOL } + ) + node.nil? ? nil : node.attributes['InResponseTo'] end + end - # @return [String] Gets the StatusCode from a Logout Response. - # - def status_code - @status_code ||= begin - node = REXML::XPath.first(document, "/p:LogoutResponse/p:Status/p:StatusCode", { "p" => PROTOCOL }) - node.nil? ? nil : node.attributes["Value"] - end + # @return [String] Gets the Issuer from the Logout Response. + # + def issuer + @issuer ||= begin + node = REXML::XPath.first( + document, + "/p:LogoutResponse/a:Issuer", + { "p" => PROTOCOL, "a" => ASSERTION } + ) + Utils.element_text(node) end + end - def status_message - @status_message ||= begin - node = REXML::XPath.first( - document, - "/p:LogoutResponse/p:Status/p:StatusMessage", - { "p" => PROTOCOL } - ) - Utils.element_text(node) - end + # @return [String] Gets the StatusCode from a Logout Response. + # + def status_code + @status_code ||= begin + node = REXML::XPath.first(document, "/p:LogoutResponse/p:Status/p:StatusCode", { "p" => PROTOCOL }) + node.nil? ? nil : node.attributes["Value"] end + end - # Aux function to validate the Logout Response - # @param collect_errors [Boolean] Stop validation when first error appears or keep validating. (if soft=true) - # @return [Boolean] TRUE if the SAML Response is valid - # @raise [ValidationError] if soft == false and validation fails - # - def validate(collect_errors = false) - reset_errors! - - validations = %i[ - valid_state? - validate_success_status - validate_structure - valid_in_response_to? - valid_issuer? - validate_signature - ] - - if collect_errors - validations.each { |validation| send(validation) } - @errors.empty? - else - validations.all? { |validation| send(validation) } - end + def status_message + @status_message ||= begin + node = REXML::XPath.first( + document, + "/p:LogoutResponse/p:Status/p:StatusMessage", + { "p" => PROTOCOL } + ) + Utils.element_text(node) end + end - private + # Aux function to validate the Logout Response + # @param collect_errors [Boolean] Stop validation when first error appears or keep validating. (if soft=true) + # @return [Boolean] TRUE if the SAML Response is valid + # @raise [ValidationError] if soft == false and validation fails + # + def validate(collect_errors = false) + reset_errors! + + validations = %i[ + valid_state? + validate_success_status + validate_structure + valid_in_response_to? + valid_issuer? + validate_signature + ] + + if collect_errors + validations.each { |validation| send(validation) } + @errors.empty? + else + validations.all? { |validation| send(validation) } + end + end - # Validates the Status of the Logout Response - # If fails, the error is added to the errors array, including the StatusCode returned and the Status Message. - # @return [Boolean] True if the Logout Response contains a Success code, otherwise False if soft=True - # @raise [ValidationError] if soft == false and validation fails - # - def validate_success_status - return true if success? + private - error_msg = 'The status code of the Logout Response was not Success' - status_error_msg = OneLogin::RubySaml::Utils.status_error_msg(error_msg, status_code, status_message) - append_error(status_error_msg) - end + # Validates the Status of the Logout Response + # If fails, the error is added to the errors array, including the StatusCode returned and the Status Message. + # @return [Boolean] True if the Logout Response contains a Success code, otherwise False if soft=True + # @raise [ValidationError] if soft == false and validation fails + # + def validate_success_status + return true if success? - # Validates the Logout Response against the specified schema. - # @return [Boolean] True if the XML is valid, otherwise False if soft=True - # @raise [ValidationError] if soft == false and validation fails - # - def validate_structure - unless valid_saml?(document, soft) - return append_error("Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd") - end + error_msg = 'The status code of the Logout Response was not Success' + status_error_msg = RubySaml::Utils.status_error_msg(error_msg, status_code, status_message) + append_error(status_error_msg) + end - true + # Validates the Logout Response against the specified schema. + # @return [Boolean] True if the XML is valid, otherwise False if soft=True + # @raise [ValidationError] if soft == false and validation fails + # + def validate_structure + unless valid_saml?(document, soft) + return append_error("Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd") end - # Validates that the Logout Response provided in the initialization is not empty, - # also check that the setting and the IdP cert were also provided - # @return [Boolean] True if the required info is found, otherwise False if soft=True - # @raise [ValidationError] if soft == false and validation fails - # - def valid_state? - return append_error("Blank logout response") if response.empty? + true + end - return append_error("No settings on logout response") if settings.nil? + # Validates that the Logout Response provided in the initialization is not empty, + # also check that the setting and the IdP cert were also provided + # @return [Boolean] True if the required info is found, otherwise False if soft=True + # @raise [ValidationError] if soft == false and validation fails + # + def valid_state? + return append_error("Blank logout response") if response.empty? - return append_error("No sp_entity_id in settings of the logout response") if settings.sp_entity_id.nil? + return append_error("No settings on logout response") if settings.nil? - if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil? && settings.idp_cert_multi.nil? - return append_error("No fingerprint or certificate on settings of the logout response") - end + return append_error("No sp_entity_id in settings of the logout response") if settings.sp_entity_id.nil? - true + if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil? && settings.idp_cert_multi.nil? + return append_error("No fingerprint or certificate on settings of the logout response") end - # Validates if a provided :matches_request_id matchs the inResponseTo value. - # @param soft [String|nil] request_id The ID of the Logout Request sent by this SP to the IdP (if was sent any) - # @return [Boolean] True if there is no request_id or it match, otherwise False if soft=True - # @raise [ValidationError] if soft == false and validation fails - # - def valid_in_response_to? - return true unless options.key? :matches_request_id - return true if options[:matches_request_id].nil? - return true unless options[:matches_request_id] != in_response_to - - error_msg = "The InResponseTo of the Logout Response: #{in_response_to}, does not match the ID of the Logout Request sent by the SP: #{options[:matches_request_id]}" - append_error(error_msg) - end + true + end + + # Validates if a provided :matches_request_id matchs the inResponseTo value. + # @param soft [String|nil] request_id The ID of the Logout Request sent by this SP to the IdP (if was sent any) + # @return [Boolean] True if there is no request_id or it match, otherwise False if soft=True + # @raise [ValidationError] if soft == false and validation fails + # + def valid_in_response_to? + return true unless options.key? :matches_request_id + return true if options[:matches_request_id].nil? + return true unless options[:matches_request_id] != in_response_to - # Validates the Issuer of the Logout Response - # @return [Boolean] True if the Issuer matchs the IdP entityId, otherwise False if soft=True - # @raise [ValidationError] if soft == false and validation fails - # - def valid_issuer? - return true if settings.idp_entity_id.nil? || issuer.nil? + error_msg = "The InResponseTo of the Logout Response: #{in_response_to}, does not match the ID of the Logout Request sent by the SP: #{options[:matches_request_id]}" + append_error(error_msg) + end - unless OneLogin::RubySaml::Utils.uri_match?(issuer, settings.idp_entity_id) - return append_error("Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>") - end - true + # Validates the Issuer of the Logout Response + # @return [Boolean] True if the Issuer matchs the IdP entityId, otherwise False if soft=True + # @raise [ValidationError] if soft == false and validation fails + # + def valid_issuer? + return true if settings.idp_entity_id.nil? || issuer.nil? + + unless RubySaml::Utils.uri_match?(issuer, settings.idp_entity_id) + return append_error("Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>") end + true + end - # Validates the Signature if it exists and the GET parameters are provided - # @return [Boolean] True if not contains a Signature or if the Signature is valid, otherwise False if soft=True - # @raise [ValidationError] if soft == false and validation fails - # - def validate_signature - return true if options.nil? - return true unless options.key? :get_params - return true unless options[:get_params].key? 'Signature' + # Validates the Signature if it exists and the GET parameters are provided + # @return [Boolean] True if not contains a Signature or if the Signature is valid, otherwise False if soft=True + # @raise [ValidationError] if soft == false and validation fails + # + def validate_signature + return true if options.nil? + return true unless options.key? :get_params + return true unless options[:get_params].key? 'Signature' - options[:raw_get_params] = OneLogin::RubySaml::Utils.prepare_raw_get_params(options[:raw_get_params], options[:get_params], settings.security[:lowercase_url_encoding]) + options[:raw_get_params] = RubySaml::Utils.prepare_raw_get_params(options[:raw_get_params], options[:get_params], settings.security[:lowercase_url_encoding]) - if options[:get_params]['SigAlg'].nil? && !options[:raw_get_params]['SigAlg'].nil? - options[:get_params]['SigAlg'] = CGI.unescape(options[:raw_get_params]['SigAlg']) - end + if options[:get_params]['SigAlg'].nil? && !options[:raw_get_params]['SigAlg'].nil? + options[:get_params]['SigAlg'] = CGI.unescape(options[:raw_get_params]['SigAlg']) + end - idp_cert = settings.get_idp_cert - idp_certs = settings.get_idp_cert_multi + idp_cert = settings.get_idp_cert + idp_certs = settings.get_idp_cert_multi - if idp_cert.nil? && (idp_certs.nil? || idp_certs[:signing].empty?) - return options.key? :relax_signature_validation - end + if idp_cert.nil? && (idp_certs.nil? || idp_certs[:signing].empty?) + return options.key? :relax_signature_validation + end - query_string = OneLogin::RubySaml::Utils.build_query_from_raw_parts( - type: 'SAMLResponse', - raw_data: options[:raw_get_params]['SAMLResponse'], - raw_relay_state: options[:raw_get_params]['RelayState'], - raw_sig_alg: options[:raw_get_params]['SigAlg'] + query_string = RubySaml::Utils.build_query_from_raw_parts( + type: 'SAMLResponse', + raw_data: options[:raw_get_params]['SAMLResponse'], + raw_relay_state: options[:raw_get_params]['RelayState'], + raw_sig_alg: options[:raw_get_params]['SigAlg'] + ) + + expired = false + if idp_certs.nil? || idp_certs[:signing].empty? + valid = RubySaml::Utils.verify_signature( + cert: idp_cert, + sig_alg: options[:get_params]['SigAlg'], + signature: options[:get_params]['Signature'], + query_string: query_string ) - - expired = false - if idp_certs.nil? || idp_certs[:signing].empty? - valid = OneLogin::RubySaml::Utils.verify_signature( - cert: idp_cert, + if valid && settings.security[:check_idp_cert_expiration] && RubySaml::Utils.is_cert_expired(idp_cert) + expired = true + end + else + valid = false + idp_certs[:signing].each do |signing_idp_cert| + valid = RubySaml::Utils.verify_signature( + cert: signing_idp_cert, sig_alg: options[:get_params]['SigAlg'], signature: options[:get_params]['Signature'], query_string: query_string ) - if valid && settings.security[:check_idp_cert_expiration] && OneLogin::RubySaml::Utils.is_cert_expired(idp_cert) + next unless valid + if settings.security[:check_idp_cert_expiration] && RubySaml::Utils.is_cert_expired(signing_idp_cert) expired = true end - else - valid = false - idp_certs[:signing].each do |signing_idp_cert| - valid = OneLogin::RubySaml::Utils.verify_signature( - cert: signing_idp_cert, - sig_alg: options[:get_params]['SigAlg'], - signature: options[:get_params]['Signature'], - query_string: query_string - ) - next unless valid - if settings.security[:check_idp_cert_expiration] && OneLogin::RubySaml::Utils.is_cert_expired(signing_idp_cert) - expired = true - end - break - end - end - - if expired - error_msg = "IdP x509 certificate expired" - return append_error(error_msg) + break end - unless valid - error_msg = "Invalid Signature on Logout Response" - return append_error(error_msg) - end - true end + if expired + error_msg = "IdP x509 certificate expired" + return append_error(error_msg) + end + unless valid + error_msg = "Invalid Signature on Logout Response" + return append_error(error_msg) + end + true end + end end diff --git a/lib/onelogin/ruby-saml/metadata.rb b/lib/onelogin/ruby-saml/metadata.rb index 5284198a0..20213c0e1 100644 --- a/lib/onelogin/ruby-saml/metadata.rb +++ b/lib/onelogin/ruby-saml/metadata.rb @@ -6,170 +6,168 @@ require "onelogin/ruby-saml/utils" # Only supports SAML 2.0 -module OneLogin - module RubySaml - - # SAML2 Metadata. XML Metadata Builder +module RubySaml + + # SAML2 Metadata. XML Metadata Builder + # + class Metadata + + # Return SP metadata based on the settings. + # @param settings [RubySaml::Settings|nil] Toolkit settings + # @param pretty_print [Boolean] Pretty print or not the response + # (No pretty print if you gonna validate the signature) + # @param valid_until [DateTime] Metadata's valid time + # @param cache_duration [Integer] Duration of the cache in seconds + # @return [String] XML Metadata of the Service Provider # - class Metadata - - # Return SP metadata based on the settings. - # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings - # @param pretty_print [Boolean] Pretty print or not the response - # (No pretty print if you gonna validate the signature) - # @param valid_until [DateTime] Metadata's valid time - # @param cache_duration [Integer] Duration of the cache in seconds - # @return [String] XML Metadata of the Service Provider - # - def generate(settings, pretty_print=false, valid_until=nil, cache_duration=nil) - meta_doc = XMLSecurity::Document.new - add_xml_declaration(meta_doc) - root = add_root_element(meta_doc, settings, valid_until, cache_duration) - sp_sso = add_sp_sso_element(root, settings) - add_sp_certificates(sp_sso, settings) - add_sp_service_elements(sp_sso, settings) - add_extras(root, settings) - embed_signature(meta_doc, settings) - output_xml(meta_doc, pretty_print) - end - - protected + def generate(settings, pretty_print=false, valid_until=nil, cache_duration=nil) + meta_doc = XMLSecurity::Document.new + add_xml_declaration(meta_doc) + root = add_root_element(meta_doc, settings, valid_until, cache_duration) + sp_sso = add_sp_sso_element(root, settings) + add_sp_certificates(sp_sso, settings) + add_sp_service_elements(sp_sso, settings) + add_extras(root, settings) + embed_signature(meta_doc, settings) + output_xml(meta_doc, pretty_print) + end - def add_xml_declaration(meta_doc) - meta_doc << REXML::XMLDecl.new('1.0', 'UTF-8') - end + protected - def add_root_element(meta_doc, settings, valid_until, cache_duration) - namespaces = { - "xmlns:md" => "urn:oasis:names:tc:SAML:2.0:metadata" - } + def add_xml_declaration(meta_doc) + meta_doc << REXML::XMLDecl.new('1.0', 'UTF-8') + end - if settings.attribute_consuming_service.configured? - namespaces["xmlns:saml"] = "urn:oasis:names:tc:SAML:2.0:assertion" - end + def add_root_element(meta_doc, settings, valid_until, cache_duration) + namespaces = { + "xmlns:md" => "urn:oasis:names:tc:SAML:2.0:metadata" + } - root = meta_doc.add_element("md:EntityDescriptor", namespaces) - root.attributes["ID"] = OneLogin::RubySaml::Utils.uuid - root.attributes["entityID"] = settings.sp_entity_id if settings.sp_entity_id - root.attributes["validUntil"] = valid_until.utc.strftime('%Y-%m-%dT%H:%M:%SZ') if valid_until - root.attributes["cacheDuration"] = "PT#{cache_duration}S" if cache_duration - root + if settings.attribute_consuming_service.configured? + namespaces["xmlns:saml"] = "urn:oasis:names:tc:SAML:2.0:assertion" end - def add_sp_sso_element(root, settings) - root.add_element "md:SPSSODescriptor", { - "protocolSupportEnumeration" => "urn:oasis:names:tc:SAML:2.0:protocol", - "AuthnRequestsSigned" => settings.security[:authn_requests_signed], - "WantAssertionsSigned" => settings.security[:want_assertions_signed] - } - end + root = meta_doc.add_element("md:EntityDescriptor", namespaces) + root.attributes["ID"] = RubySaml::Utils.uuid + root.attributes["entityID"] = settings.sp_entity_id if settings.sp_entity_id + root.attributes["validUntil"] = valid_until.utc.strftime('%Y-%m-%dT%H:%M:%SZ') if valid_until + root.attributes["cacheDuration"] = "PT#{cache_duration}S" if cache_duration + root + end - # Add KeyDescriptor elements for SP certificates. - def add_sp_certificates(sp_sso, settings) - certs = settings.get_sp_certs + def add_sp_sso_element(root, settings) + root.add_element "md:SPSSODescriptor", { + "protocolSupportEnumeration" => "urn:oasis:names:tc:SAML:2.0:protocol", + "AuthnRequestsSigned" => settings.security[:authn_requests_signed], + "WantAssertionsSigned" => settings.security[:want_assertions_signed] + } + end - certs[:signing].each { |cert, _| add_sp_cert_element(sp_sso, cert, :signing) } + # Add KeyDescriptor elements for SP certificates. + def add_sp_certificates(sp_sso, settings) + certs = settings.get_sp_certs - if settings.security[:want_assertions_encrypted] - certs[:encryption].each { |cert, _| add_sp_cert_element(sp_sso, cert, :encryption) } - end + certs[:signing].each { |cert, _| add_sp_cert_element(sp_sso, cert, :signing) } - sp_sso + if settings.security[:want_assertions_encrypted] + certs[:encryption].each { |cert, _| add_sp_cert_element(sp_sso, cert, :encryption) } end - def add_sp_service_elements(sp_sso, settings) - if settings.single_logout_service_url - sp_sso.add_element "md:SingleLogoutService", { - "Binding" => settings.single_logout_service_binding, - "Location" => settings.single_logout_service_url, - "ResponseLocation" => settings.single_logout_service_url - } - end + sp_sso + end - if settings.name_identifier_format - nameid = sp_sso.add_element "md:NameIDFormat" - nameid.text = settings.name_identifier_format - end + def add_sp_service_elements(sp_sso, settings) + if settings.single_logout_service_url + sp_sso.add_element "md:SingleLogoutService", { + "Binding" => settings.single_logout_service_binding, + "Location" => settings.single_logout_service_url, + "ResponseLocation" => settings.single_logout_service_url + } + end - if settings.assertion_consumer_service_url - sp_sso.add_element "md:AssertionConsumerService", { - "Binding" => settings.assertion_consumer_service_binding, - "Location" => settings.assertion_consumer_service_url, - "isDefault" => true, - "index" => 0 - } - end + if settings.name_identifier_format + nameid = sp_sso.add_element "md:NameIDFormat" + nameid.text = settings.name_identifier_format + end - if settings.attribute_consuming_service.configured? - sp_acs = sp_sso.add_element "md:AttributeConsumingService", { - "isDefault" => "true", - "index" => settings.attribute_consuming_service.index - } - srv_name = sp_acs.add_element "md:ServiceName", { - "xml:lang" => "en" + if settings.assertion_consumer_service_url + sp_sso.add_element "md:AssertionConsumerService", { + "Binding" => settings.assertion_consumer_service_binding, + "Location" => settings.assertion_consumer_service_url, + "isDefault" => true, + "index" => 0 + } + end + + if settings.attribute_consuming_service.configured? + sp_acs = sp_sso.add_element "md:AttributeConsumingService", { + "isDefault" => "true", + "index" => settings.attribute_consuming_service.index + } + srv_name = sp_acs.add_element "md:ServiceName", { + "xml:lang" => "en" + } + srv_name.text = settings.attribute_consuming_service.name + settings.attribute_consuming_service.attributes.each do |attribute| + sp_req_attr = sp_acs.add_element "md:RequestedAttribute", { + "NameFormat" => attribute[:name_format], + "Name" => attribute[:name], + "FriendlyName" => attribute[:friendly_name], + "isRequired" => attribute[:is_required] || false } - srv_name.text = settings.attribute_consuming_service.name - settings.attribute_consuming_service.attributes.each do |attribute| - sp_req_attr = sp_acs.add_element "md:RequestedAttribute", { - "NameFormat" => attribute[:name_format], - "Name" => attribute[:name], - "FriendlyName" => attribute[:friendly_name], - "isRequired" => attribute[:is_required] || false - } - next if attribute[:attribute_value].nil? - - Array(attribute[:attribute_value]).each do |value| - sp_attr_val = sp_req_attr.add_element "saml:AttributeValue" - sp_attr_val.text = value.to_s - end + next if attribute[:attribute_value].nil? + + Array(attribute[:attribute_value]).each do |value| + sp_attr_val = sp_req_attr.add_element "saml:AttributeValue" + sp_attr_val.text = value.to_s end end - - # With OpenSSO, it might be required to also include - # - # - - sp_sso end - # can be overridden in subclass - def add_extras(root, _settings) - root - end + # With OpenSSO, it might be required to also include + # + # - def embed_signature(meta_doc, settings) - return unless settings.security[:metadata_signed] + sp_sso + end - cert, private_key = settings.get_sp_signing_pair - return unless private_key && cert + # can be overridden in subclass + def add_extras(root, _settings) + root + end - meta_doc.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method]) - end + def embed_signature(meta_doc, settings) + return unless settings.security[:metadata_signed] - def output_xml(meta_doc, pretty_print) - ret = +'' + cert, private_key = settings.get_sp_signing_pair + return unless private_key && cert - # pretty print the XML so IdP administrators can easily see what the SP supports - if pretty_print - meta_doc.write(ret, 1) - else - ret = meta_doc.to_s - end + meta_doc.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method]) + end + + def output_xml(meta_doc, pretty_print) + ret = +'' - ret + # pretty print the XML so IdP administrators can easily see what the SP supports + if pretty_print + meta_doc.write(ret, 1) + else + ret = meta_doc.to_s end - private + ret + end - def add_sp_cert_element(sp_sso, cert, use) - return unless cert - cert_text = Base64.encode64(cert.to_der).gsub("\n", '') - kd = sp_sso.add_element "md:KeyDescriptor", { "use" => use.to_s } - ki = kd.add_element "ds:KeyInfo", { "xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#" } - xd = ki.add_element "ds:X509Data" - xc = xd.add_element "ds:X509Certificate" - xc.text = cert_text - end + private + + def add_sp_cert_element(sp_sso, cert, use) + return unless cert + cert_text = Base64.encode64(cert.to_der).gsub("\n", '') + kd = sp_sso.add_element "md:KeyDescriptor", { "use" => use.to_s } + ki = kd.add_element "ds:KeyInfo", { "xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#" } + xd = ki.add_element "ds:X509Data" + xc = xd.add_element "ds:X509Certificate" + xc.text = cert_text end end end diff --git a/lib/onelogin/ruby-saml/response.rb b/lib/onelogin/ruby-saml/response.rb index 90664d772..31e6d290e 100644 --- a/lib/onelogin/ruby-saml/response.rb +++ b/lib/onelogin/ruby-saml/response.rb @@ -7,1045 +7,1043 @@ require "nokogiri" # Only supports SAML 2.0 -module OneLogin - module RubySaml - - # SAML2 Authentication Response. SAML Response +module RubySaml + + # SAML2 Authentication Response. SAML Response + # + class Response < SamlMessage + include ErrorHandling + + ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion" + PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol" + DSIG = "http://www.w3.org/2000/09/xmldsig#" + XENC = "http://www.w3.org/2001/04/xmlenc#" + + # TODO: Settings should probably be initialized too... WDYT? + + # RubySaml::Settings Toolkit settings + attr_accessor :settings + + attr_reader :document + attr_reader :decrypted_document + attr_reader :response + attr_reader :options + + attr_accessor :soft + + # Response available options + # This is not a whitelist to allow people extending RubySaml:Response + # and pass custom options + AVAILABLE_OPTIONS = %i[ + allowed_clock_drift check_duplicated_attributes matches_request_id settings skip_audience skip_authnstatement skip_conditions + skip_destination skip_recipient_check skip_subject_confirmation + ].freeze + # TODO: Update the comment on initialize to describe every option + + # Constructs the SAML Response. A Response Object that is an extension of the SamlMessage class. + # @param response [String] A UUEncoded SAML response from the IdP. + # @param options [Hash] :settings to provide the RubySaml::Settings object + # Or some options for the response validation process like skip the conditions validation + # with the :skip_conditions, or allow a clock_drift when checking dates with :allowed_clock_drift + # or :matches_request_id that will validate that the response matches the ID of the request, + # or skip the subject confirmation validation with the :skip_subject_confirmation option + # or skip the recipient validation of the subject confirmation element with :skip_recipient_check option + # or skip the audience validation with :skip_audience option # - class Response < SamlMessage - include ErrorHandling - - ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion" - PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol" - DSIG = "http://www.w3.org/2000/09/xmldsig#" - XENC = "http://www.w3.org/2001/04/xmlenc#" - - # TODO: Settings should probably be initialized too... WDYT? - - # OneLogin::RubySaml::Settings Toolkit settings - attr_accessor :settings - - attr_reader :document - attr_reader :decrypted_document - attr_reader :response - attr_reader :options - - attr_accessor :soft - - # Response available options - # This is not a whitelist to allow people extending OneLogin::RubySaml:Response - # and pass custom options - AVAILABLE_OPTIONS = %i[ - allowed_clock_drift check_duplicated_attributes matches_request_id settings skip_audience skip_authnstatement skip_conditions - skip_destination skip_recipient_check skip_subject_confirmation - ].freeze - # TODO: Update the comment on initialize to describe every option - - # Constructs the SAML Response. A Response Object that is an extension of the SamlMessage class. - # @param response [String] A UUEncoded SAML response from the IdP. - # @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object - # Or some options for the response validation process like skip the conditions validation - # with the :skip_conditions, or allow a clock_drift when checking dates with :allowed_clock_drift - # or :matches_request_id that will validate that the response matches the ID of the request, - # or skip the subject confirmation validation with the :skip_subject_confirmation option - # or skip the recipient validation of the subject confirmation element with :skip_recipient_check option - # or skip the audience validation with :skip_audience option - # - def initialize(response, options = {}) - raise ArgumentError.new("Response cannot be nil") if response.nil? - - @errors = [] - - @options = options - @soft = true - unless options[:settings].nil? - @settings = options[:settings] - unless @settings.soft.nil? - @soft = @settings.soft - end - end + def initialize(response, options = {}) + raise ArgumentError.new("Response cannot be nil") if response.nil? - @response = decode_raw_saml(response, settings) - @document = XMLSecurity::SignedDocument.new(@response, @errors) + @errors = [] - if assertion_encrypted? - @decrypted_document = generate_decrypted_document + @options = options + @soft = true + unless options[:settings].nil? + @settings = options[:settings] + unless @settings.soft.nil? + @soft = @settings.soft end - - super() end - # Validates the SAML Response with the default values (soft = true) - # @param collect_errors [Boolean] Stop validation when first error appears or keep validating. (if soft=true) - # @return [Boolean] TRUE if the SAML Response is valid - # - def is_valid?(collect_errors = false) - validate(collect_errors) - end + @response = decode_raw_saml(response, settings) + @document = XMLSecurity::SignedDocument.new(@response, @errors) - # @return [String] the NameID provided by the SAML response from the IdP. - # - def name_id - @name_id ||= Utils.element_text(name_id_node) + if assertion_encrypted? + @decrypted_document = generate_decrypted_document end - alias_method :nameid, :name_id + super() + end - # @return [String] the NameID Format provided by the SAML response from the IdP. - # - def name_id_format - @name_id_format ||= - if name_id_node&.attribute("Format") - name_id_node.attribute("Format").value - end - end + # Validates the SAML Response with the default values (soft = true) + # @param collect_errors [Boolean] Stop validation when first error appears or keep validating. (if soft=true) + # @return [Boolean] TRUE if the SAML Response is valid + # + def is_valid?(collect_errors = false) + validate(collect_errors) + end - alias_method :nameid_format, :name_id_format + # @return [String] the NameID provided by the SAML response from the IdP. + # + def name_id + @name_id ||= Utils.element_text(name_id_node) + end - # @return [String] the NameID SPNameQualifier provided by the SAML response from the IdP. - # - def name_id_spnamequalifier - @name_id_spnamequalifier ||= - if name_id_node&.attribute("SPNameQualifier") - name_id_node.attribute("SPNameQualifier").value - end - end + alias_method :nameid, :name_id - # @return [String] the NameID NameQualifier provided by the SAML response from the IdP. - # - def name_id_namequalifier - @name_id_namequalifier ||= - if name_id_node&.attribute("NameQualifier") - name_id_node.attribute("NameQualifier").value - end - end + # @return [String] the NameID Format provided by the SAML response from the IdP. + # + def name_id_format + @name_id_format ||= + if name_id_node&.attribute("Format") + name_id_node.attribute("Format").value + end + end - # Gets the SessionIndex from the AuthnStatement. - # Could be used to be stored in the local session in order - # to be used in a future Logout Request that the SP could - # send to the IdP, to set what specific session must be deleted - # @return [String] SessionIndex Value - # - def sessionindex - @sessionindex ||= begin - node = xpath_first_from_signed_assertion('/a:AuthnStatement') - node.nil? ? nil : node.attributes['SessionIndex'] + alias_method :nameid_format, :name_id_format + + # @return [String] the NameID SPNameQualifier provided by the SAML response from the IdP. + # + def name_id_spnamequalifier + @name_id_spnamequalifier ||= + if name_id_node&.attribute("SPNameQualifier") + name_id_node.attribute("SPNameQualifier").value + end + end + + # @return [String] the NameID NameQualifier provided by the SAML response from the IdP. + # + def name_id_namequalifier + @name_id_namequalifier ||= + if name_id_node&.attribute("NameQualifier") + name_id_node.attribute("NameQualifier").value end + end + + # Gets the SessionIndex from the AuthnStatement. + # Could be used to be stored in the local session in order + # to be used in a future Logout Request that the SP could + # send to the IdP, to set what specific session must be deleted + # @return [String] SessionIndex Value + # + def sessionindex + @sessionindex ||= begin + node = xpath_first_from_signed_assertion('/a:AuthnStatement') + node.nil? ? nil : node.attributes['SessionIndex'] end + end - # Gets the Attributes from the AttributeStatement element. - # - # All attributes can be iterated over +attributes.each+ or returned as array by +attributes.all+ - # For backwards compatibility ruby-saml returns by default only the first value for a given attribute with - # attributes['name'] - # To get all of the attributes, use: - # attributes.multi('name') - # Or turn off the compatibility: - # OneLogin::RubySaml::Attributes.single_value_compatibility = false - # Now this will return an array: - # attributes['name'] - # - # @return [Attributes] OneLogin::RubySaml::Attributes enumerable collection. - # @raise [ValidationError] if there are 2+ Attribute with the same Name - # - def attributes - @attr_statements ||= begin - attributes = Attributes.new - - stmt_elements = xpath_from_signed_assertion('/a:AttributeStatement') - stmt_elements.each do |stmt_element| - stmt_element.elements.each do |attr_element| - if attr_element.name == "EncryptedAttribute" - node = decrypt_attribute(attr_element.dup) - else - node = attr_element - end + # Gets the Attributes from the AttributeStatement element. + # + # All attributes can be iterated over +attributes.each+ or returned as array by +attributes.all+ + # For backwards compatibility ruby-saml returns by default only the first value for a given attribute with + # attributes['name'] + # To get all of the attributes, use: + # attributes.multi('name') + # Or turn off the compatibility: + # RubySaml::Attributes.single_value_compatibility = false + # Now this will return an array: + # attributes['name'] + # + # @return [Attributes] RubySaml::Attributes enumerable collection. + # @raise [ValidationError] if there are 2+ Attribute with the same Name + # + def attributes + @attr_statements ||= begin + attributes = Attributes.new + + stmt_elements = xpath_from_signed_assertion('/a:AttributeStatement') + stmt_elements.each do |stmt_element| + stmt_element.elements.each do |attr_element| + if attr_element.name == "EncryptedAttribute" + node = decrypt_attribute(attr_element.dup) + else + node = attr_element + end - name = node.attributes["Name"] + name = node.attributes["Name"] - if options[:check_duplicated_attributes] && attributes.include?(name) - raise ValidationError.new("Found an Attribute element with duplicated Name") - end + if options[:check_duplicated_attributes] && attributes.include?(name) + raise ValidationError.new("Found an Attribute element with duplicated Name") + end - values = node.elements.collect do |e| - if e.elements.nil? || e.elements.empty? - # SAMLCore requires that nil AttributeValues MUST contain xsi:nil XML attribute set to "true" or "1" - # otherwise the value is to be regarded as empty. - %w[true 1].include?(e.attributes['xsi:nil']) ? nil : Utils.element_text(e) - # explicitly support saml2:NameID with saml2:NameQualifier if supplied in attributes - # this is useful for allowing eduPersonTargetedId to be passed as an opaque identifier to use to - # identify the subject in an SP rather than email or other less opaque attributes - # NameQualifier, if present is prefixed with a "/" to the value - else - REXML::XPath.match(e,'a:NameID', { "a" => ASSERTION }).collect do |n| - base_path = n.attributes['NameQualifier'] ? "#{n.attributes['NameQualifier']}/" : '' - "#{base_path}#{Utils.element_text(n)}" - end + values = node.elements.collect do |e| + if e.elements.nil? || e.elements.empty? + # SAMLCore requires that nil AttributeValues MUST contain xsi:nil XML attribute set to "true" or "1" + # otherwise the value is to be regarded as empty. + %w[true 1].include?(e.attributes['xsi:nil']) ? nil : Utils.element_text(e) + # explicitly support saml2:NameID with saml2:NameQualifier if supplied in attributes + # this is useful for allowing eduPersonTargetedId to be passed as an opaque identifier to use to + # identify the subject in an SP rather than email or other less opaque attributes + # NameQualifier, if present is prefixed with a "/" to the value + else + REXML::XPath.match(e,'a:NameID', { "a" => ASSERTION }).collect do |n| + base_path = n.attributes['NameQualifier'] ? "#{n.attributes['NameQualifier']}/" : '' + "#{base_path}#{Utils.element_text(n)}" end end - - attributes.add(name, values.flatten) end - end - attributes - end - end - # Gets the SessionNotOnOrAfter from the AuthnStatement. - # Could be used to set the local session expiration (expire at latest) - # @return [String] The SessionNotOnOrAfter value - # - def session_expires_at - @expires_at ||= begin - node = xpath_first_from_signed_assertion('/a:AuthnStatement') - node.nil? ? nil : parse_time(node, "SessionNotOnOrAfter") + attributes.add(name, values.flatten) + end end + attributes end + end - # Checks if the Status has the "Success" code - # @return [Boolean] True if the StatusCode is Sucess - # - def success? - status_code == "urn:oasis:names:tc:SAML:2.0:status:Success" + # Gets the SessionNotOnOrAfter from the AuthnStatement. + # Could be used to set the local session expiration (expire at latest) + # @return [String] The SessionNotOnOrAfter value + # + def session_expires_at + @expires_at ||= begin + node = xpath_first_from_signed_assertion('/a:AuthnStatement') + node.nil? ? nil : parse_time(node, "SessionNotOnOrAfter") end + end - # @return [String] StatusCode value from a SAML Response. - # - def status_code - @status_code ||= begin - nodes = REXML::XPath.match( - document, - "/p:Response/p:Status/p:StatusCode", - { "p" => PROTOCOL } - ) - if nodes.size == 1 - node = nodes[0] - code = node.attributes["Value"] if node&.attributes - - unless code == "urn:oasis:names:tc:SAML:2.0:status:Success" - nodes = REXML::XPath.match( - document, - "/p:Response/p:Status/p:StatusCode/p:StatusCode", - { "p" => PROTOCOL } - ) - statuses = nodes.collect do |inner_node| - inner_node.attributes["Value"] - end + # Checks if the Status has the "Success" code + # @return [Boolean] True if the StatusCode is Sucess + # + def success? + status_code == "urn:oasis:names:tc:SAML:2.0:status:Success" + end - code = [code, statuses].flatten.join(" | ") + # @return [String] StatusCode value from a SAML Response. + # + def status_code + @status_code ||= begin + nodes = REXML::XPath.match( + document, + "/p:Response/p:Status/p:StatusCode", + { "p" => PROTOCOL } + ) + if nodes.size == 1 + node = nodes[0] + code = node.attributes["Value"] if node&.attributes + + unless code == "urn:oasis:names:tc:SAML:2.0:status:Success" + nodes = REXML::XPath.match( + document, + "/p:Response/p:Status/p:StatusCode/p:StatusCode", + { "p" => PROTOCOL } + ) + statuses = nodes.collect do |inner_node| + inner_node.attributes["Value"] end - code + code = [code, statuses].flatten.join(" | ") end - end - end - # @return [String] the StatusMessage value from a SAML Response. - # - def status_message - @status_message ||= begin - nodes = REXML::XPath.match( - document, - "/p:Response/p:Status/p:StatusMessage", - { "p" => PROTOCOL } - ) - - Utils.element_text(nodes.first) if nodes.size == 1 + code end end + end - # Gets the Condition Element of the SAML Response if exists. - # (returns the first node that matches the supplied xpath) - # @return [REXML::Element] Conditions Element if exists - # - def conditions - @conditions ||= xpath_first_from_signed_assertion('/a:Conditions') - end + # @return [String] the StatusMessage value from a SAML Response. + # + def status_message + @status_message ||= begin + nodes = REXML::XPath.match( + document, + "/p:Response/p:Status/p:StatusMessage", + { "p" => PROTOCOL } + ) - # Gets the NotBefore Condition Element value. - # @return [Time] The NotBefore value in Time format - # - def not_before - @not_before ||= parse_time(conditions, "NotBefore") + Utils.element_text(nodes.first) if nodes.size == 1 end + end - # Gets the NotOnOrAfter Condition Element value. - # @return [Time] The NotOnOrAfter value in Time format - # - def not_on_or_after - @not_on_or_after ||= parse_time(conditions, "NotOnOrAfter") - end + # Gets the Condition Element of the SAML Response if exists. + # (returns the first node that matches the supplied xpath) + # @return [REXML::Element] Conditions Element if exists + # + def conditions + @conditions ||= xpath_first_from_signed_assertion('/a:Conditions') + end - # Gets the Issuers (from Response and Assertion). - # (returns the first node that matches the supplied xpath from the Response and from the Assertion) - # @return [Array] Array with the Issuers (REXML::Element) - # - def issuers - @issuers ||= begin - issuer_response_nodes = REXML::XPath.match( - document, - "/p:Response/a:Issuer", - { "p" => PROTOCOL, "a" => ASSERTION } - ) - - unless issuer_response_nodes.size == 1 - error_msg = "Issuer of the Response not found or multiple." - raise ValidationError.new(error_msg) - end + # Gets the NotBefore Condition Element value. + # @return [Time] The NotBefore value in Time format + # + def not_before + @not_before ||= parse_time(conditions, "NotBefore") + end - issuer_assertion_nodes = xpath_from_signed_assertion("/a:Issuer") - unless issuer_assertion_nodes.size == 1 - error_msg = "Issuer of the Assertion not found or multiple." - raise ValidationError.new(error_msg) - end + # Gets the NotOnOrAfter Condition Element value. + # @return [Time] The NotOnOrAfter value in Time format + # + def not_on_or_after + @not_on_or_after ||= parse_time(conditions, "NotOnOrAfter") + end - nodes = issuer_response_nodes + issuer_assertion_nodes - nodes.filter_map { |node| Utils.element_text(node) }.uniq - end - end + # Gets the Issuers (from Response and Assertion). + # (returns the first node that matches the supplied xpath from the Response and from the Assertion) + # @return [Array] Array with the Issuers (REXML::Element) + # + def issuers + @issuers ||= begin + issuer_response_nodes = REXML::XPath.match( + document, + "/p:Response/a:Issuer", + { "p" => PROTOCOL, "a" => ASSERTION } + ) - # @return [String|nil] The InResponseTo attribute from the SAML Response. - # - def in_response_to - @in_response_to ||= begin - node = REXML::XPath.first( - document, - "/p:Response", - { "p" => PROTOCOL } - ) - node.nil? ? nil : node.attributes['InResponseTo'] + unless issuer_response_nodes.size == 1 + error_msg = "Issuer of the Response not found or multiple." + raise ValidationError.new(error_msg) end - end - # @return [String|nil] Destination attribute from the SAML Response. - # - def destination - @destination ||= begin - node = REXML::XPath.first( - document, - "/p:Response", - { "p" => PROTOCOL } - ) - node.nil? ? nil : node.attributes['Destination'] + issuer_assertion_nodes = xpath_from_signed_assertion("/a:Issuer") + unless issuer_assertion_nodes.size == 1 + error_msg = "Issuer of the Assertion not found or multiple." + raise ValidationError.new(error_msg) end - end - # @return [Array] The Audience elements from the Contitions of the SAML Response. - # - def audiences - @audiences ||= begin - nodes = xpath_from_signed_assertion('/a:Conditions/a:AudienceRestriction/a:Audience') - nodes.map { |node| Utils.element_text(node) }.reject(&:empty?) - end + nodes = issuer_response_nodes + issuer_assertion_nodes + nodes.filter_map { |node| Utils.element_text(node) }.uniq end + end - # returns the allowed clock drift on timing validation - # @return [Float] - def allowed_clock_drift - options[:allowed_clock_drift].to_f.abs + Float::EPSILON + # @return [String|nil] The InResponseTo attribute from the SAML Response. + # + def in_response_to + @in_response_to ||= begin + node = REXML::XPath.first( + document, + "/p:Response", + { "p" => PROTOCOL } + ) + node.nil? ? nil : node.attributes['InResponseTo'] end + end - # Checks if the SAML Response contains or not an EncryptedAssertion element - # @return [Boolean] True if the SAML Response contains an EncryptedAssertion element - # - def assertion_encrypted? - !REXML::XPath.first( + # @return [String|nil] Destination attribute from the SAML Response. + # + def destination + @destination ||= begin + node = REXML::XPath.first( document, - "(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)", - { "p" => PROTOCOL, "a" => ASSERTION } - ).nil? + "/p:Response", + { "p" => PROTOCOL } + ) + node.nil? ? nil : node.attributes['Destination'] end + end - def response_id - id(document) + # @return [Array] The Audience elements from the Contitions of the SAML Response. + # + def audiences + @audiences ||= begin + nodes = xpath_from_signed_assertion('/a:Conditions/a:AudienceRestriction/a:Audience') + nodes.map { |node| Utils.element_text(node) }.reject(&:empty?) end + end - def assertion_id - @assertion_id ||= begin - node = xpath_first_from_signed_assertion("") - node.nil? ? nil : node.attributes['ID'] - end - end + # returns the allowed clock drift on timing validation + # @return [Float] + def allowed_clock_drift + options[:allowed_clock_drift].to_f.abs + Float::EPSILON + end - private - - # Validates the SAML Response (calls several validation methods) - # @param collect_errors [Boolean] Stop validation when first error appears or keep validating. (if soft=true) - # @return [Boolean] True if the SAML Response is valid, otherwise False if soft=True - # @raise [ValidationError] if soft == false and validation fails - # - def validate(collect_errors = false) - reset_errors! - return false unless validate_response_state - - validations = %i[ - validate_version - validate_id - validate_success_status - validate_num_assertion - validate_no_duplicated_attributes - validate_signed_elements - validate_structure - validate_in_response_to - validate_one_conditions - validate_conditions - validate_one_authnstatement - validate_audience - validate_destination - validate_issuer - validate_session_expiration - validate_subject_confirmation - validate_name_id - validate_signature - ] - - if collect_errors - validations.each { |validation| send(validation) } - @errors.empty? - else - validations.all? { |validation| send(validation) } - end + # Checks if the SAML Response contains or not an EncryptedAssertion element + # @return [Boolean] True if the SAML Response contains an EncryptedAssertion element + # + def assertion_encrypted? + !REXML::XPath.first( + document, + "(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)", + { "p" => PROTOCOL, "a" => ASSERTION } + ).nil? + end + + def response_id + id(document) + end + + def assertion_id + @assertion_id ||= begin + node = xpath_first_from_signed_assertion("") + node.nil? ? nil : node.attributes['ID'] end + end - # Validates the Status of the SAML Response - # @return [Boolean] True if the SAML Response contains a Success code, otherwise False if soft == false - # @raise [ValidationError] if soft == false and validation fails - # - def validate_success_status - return true if success? + private - error_msg = 'The status code of the Response was not Success' - status_error_msg = OneLogin::RubySaml::Utils.status_error_msg(error_msg, status_code, status_message) - append_error(status_error_msg) + # Validates the SAML Response (calls several validation methods) + # @param collect_errors [Boolean] Stop validation when first error appears or keep validating. (if soft=true) + # @return [Boolean] True if the SAML Response is valid, otherwise False if soft=True + # @raise [ValidationError] if soft == false and validation fails + # + def validate(collect_errors = false) + reset_errors! + return false unless validate_response_state + + validations = %i[ + validate_version + validate_id + validate_success_status + validate_num_assertion + validate_no_duplicated_attributes + validate_signed_elements + validate_structure + validate_in_response_to + validate_one_conditions + validate_conditions + validate_one_authnstatement + validate_audience + validate_destination + validate_issuer + validate_session_expiration + validate_subject_confirmation + validate_name_id + validate_signature + ] + + if collect_errors + validations.each { |validation| send(validation) } + @errors.empty? + else + validations.all? { |validation| send(validation) } end + end - # Validates the SAML Response against the specified schema. - # @return [Boolean] True if the XML is valid, otherwise False if soft=True - # @raise [ValidationError] if soft == false and validation fails - # - def validate_structure - structure_error_msg = "Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd" - unless valid_saml?(document, soft) - return append_error(structure_error_msg) - end + # Validates the Status of the SAML Response + # @return [Boolean] True if the SAML Response contains a Success code, otherwise False if soft == false + # @raise [ValidationError] if soft == false and validation fails + # + def validate_success_status + return true if success? - if !decrypted_document.nil? && !valid_saml?(decrypted_document, soft) - return append_error(structure_error_msg) - end + error_msg = 'The status code of the Response was not Success' + status_error_msg = RubySaml::Utils.status_error_msg(error_msg, status_code, status_message) + append_error(status_error_msg) + end - true + # Validates the SAML Response against the specified schema. + # @return [Boolean] True if the XML is valid, otherwise False if soft=True + # @raise [ValidationError] if soft == false and validation fails + # + def validate_structure + structure_error_msg = "Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd" + unless valid_saml?(document, soft) + return append_error(structure_error_msg) end - # Validates that the SAML Response provided in the initialization is not empty, - # also check that the setting and the IdP cert were also provided - # @return [Boolean] True if the required info is found, false otherwise - # - def validate_response_state - return append_error("Blank response") if response.nil? || response.empty? + if !decrypted_document.nil? && !valid_saml?(decrypted_document, soft) + return append_error(structure_error_msg) + end - return append_error("No settings on response") if settings.nil? + true + end - if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil? && settings.idp_cert_multi.nil? - return append_error("No fingerprint or certificate on settings") - end + # Validates that the SAML Response provided in the initialization is not empty, + # also check that the setting and the IdP cert were also provided + # @return [Boolean] True if the required info is found, false otherwise + # + def validate_response_state + return append_error("Blank response") if response.nil? || response.empty? - true - end + return append_error("No settings on response") if settings.nil? - # Validates that the SAML Response contains an ID - # If fails, the error is added to the errors array. - # @return [Boolean] True if the SAML Response contains an ID, otherwise returns False - # - def validate_id - return true if response_id - append_error("Missing ID attribute on SAML Response") + if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil? && settings.idp_cert_multi.nil? + return append_error("No fingerprint or certificate on settings") end - # Validates the SAML version (2.0) - # If fails, the error is added to the errors array. - # @return [Boolean] True if the SAML Response is 2.0, otherwise returns False - # - def validate_version - return true if version(document) == "2.0" - append_error("Unsupported SAML version") + true + end + + # Validates that the SAML Response contains an ID + # If fails, the error is added to the errors array. + # @return [Boolean] True if the SAML Response contains an ID, otherwise returns False + # + def validate_id + return true if response_id + append_error("Missing ID attribute on SAML Response") + end + + # Validates the SAML version (2.0) + # If fails, the error is added to the errors array. + # @return [Boolean] True if the SAML Response is 2.0, otherwise returns False + # + def validate_version + return true if version(document) == "2.0" + append_error("Unsupported SAML version") + end + + # Validates that the SAML Response only contains a single Assertion (encrypted or not). + # If fails, the error is added to the errors array. + # @return [Boolean] True if the SAML Response contains one unique Assertion, otherwise False + # + def validate_num_assertion + error_msg = "SAML Response must contain 1 assertion" + assertions = REXML::XPath.match( + document, + "//a:Assertion", + { "a" => ASSERTION } + ) + encrypted_assertions = REXML::XPath.match( + document, + "//a:EncryptedAssertion", + { "a" => ASSERTION } + ) + + unless assertions.size + encrypted_assertions.size == 1 + return append_error(error_msg) end - # Validates that the SAML Response only contains a single Assertion (encrypted or not). - # If fails, the error is added to the errors array. - # @return [Boolean] True if the SAML Response contains one unique Assertion, otherwise False - # - def validate_num_assertion - error_msg = "SAML Response must contain 1 assertion" + unless decrypted_document.nil? assertions = REXML::XPath.match( - document, + decrypted_document, "//a:Assertion", { "a" => ASSERTION } ) - encrypted_assertions = REXML::XPath.match( - document, - "//a:EncryptedAssertion", - { "a" => ASSERTION } - ) - - unless assertions.size + encrypted_assertions.size == 1 + unless assertions.size == 1 return append_error(error_msg) end - - unless decrypted_document.nil? - assertions = REXML::XPath.match( - decrypted_document, - "//a:Assertion", - { "a" => ASSERTION } - ) - unless assertions.size == 1 - return append_error(error_msg) - end - end - - true end - # Validates that there are not duplicated attributes - # If fails, the error is added to the errors array - # @return [Boolean] True if there are no duplicated attribute elements, otherwise False if soft=True - # @raise [ValidationError] if soft == false and validation fails - # - def validate_no_duplicated_attributes - if options[:check_duplicated_attributes] - begin - attributes - rescue ValidationError => e - return append_error(e.message) - end - end + true + end - true + # Validates that there are not duplicated attributes + # If fails, the error is added to the errors array + # @return [Boolean] True if there are no duplicated attribute elements, otherwise False if soft=True + # @raise [ValidationError] if soft == false and validation fails + # + def validate_no_duplicated_attributes + if options[:check_duplicated_attributes] + begin + attributes + rescue ValidationError => e + return append_error(e.message) + end end - # Validates the Signed elements - # If fails, the error is added to the errors array - # @return [Boolean] True if there is 1 or 2 Elements signed in the SAML Response - # an are a Response or an Assertion Element, otherwise False if soft=True - # - def validate_signed_elements - signature_nodes = REXML::XPath.match( - decrypted_document.nil? ? document : decrypted_document, - "//ds:Signature", - {"ds"=>DSIG} - ) - signed_elements = [] - verified_seis = [] - verified_ids = [] - signature_nodes.each do |signature_node| - signed_element = signature_node.parent.name - if signed_element != 'Response' && signed_element != 'Assertion' - return append_error("Invalid Signature Element '#{signed_element}'. SAML Response rejected") - end - - if signature_node.parent.attributes['ID'].nil? - return append_error("Signed Element must contain an ID. SAML Response rejected") - end + true + end - id = signature_node.parent.attributes.get_attribute("ID").value - if verified_ids.include?(id) - return append_error("Duplicated ID. SAML Response rejected") - end - verified_ids.push(id) + # Validates the Signed elements + # If fails, the error is added to the errors array + # @return [Boolean] True if there is 1 or 2 Elements signed in the SAML Response + # an are a Response or an Assertion Element, otherwise False if soft=True + # + def validate_signed_elements + signature_nodes = REXML::XPath.match( + decrypted_document.nil? ? document : decrypted_document, + "//ds:Signature", + {"ds"=>DSIG} + ) + signed_elements = [] + verified_seis = [] + verified_ids = [] + signature_nodes.each do |signature_node| + signed_element = signature_node.parent.name + if signed_element != 'Response' && signed_element != 'Assertion' + return append_error("Invalid Signature Element '#{signed_element}'. SAML Response rejected") + end - # Check that reference URI matches the parent ID and no duplicate References or IDs - ref = REXML::XPath.first(signature_node, ".//ds:Reference", {"ds"=>DSIG}) - if ref - uri = ref.attributes.get_attribute("URI") - if uri && !uri.value.empty? - sei = uri.value[1..] + if signature_node.parent.attributes['ID'].nil? + return append_error("Signed Element must contain an ID. SAML Response rejected") + end - unless sei == id - return append_error("Found an invalid Signed Element. SAML Response rejected") - end + id = signature_node.parent.attributes.get_attribute("ID").value + if verified_ids.include?(id) + return append_error("Duplicated ID. SAML Response rejected") + end + verified_ids.push(id) - if verified_seis.include?(sei) - return append_error("Duplicated Reference URI. SAML Response rejected") - end + # Check that reference URI matches the parent ID and no duplicate References or IDs + ref = REXML::XPath.first(signature_node, ".//ds:Reference", {"ds"=>DSIG}) + if ref + uri = ref.attributes.get_attribute("URI") + if uri && !uri.value.empty? + sei = uri.value[1..] - verified_seis.push(sei) + unless sei == id + return append_error("Found an invalid Signed Element. SAML Response rejected") end - end - signed_elements << signed_element - end + if verified_seis.include?(sei) + return append_error("Duplicated Reference URI. SAML Response rejected") + end - unless signature_nodes.length < 3 && !signed_elements.empty? - return append_error("Found an unexpected number of Signature Element. SAML Response rejected") + verified_seis.push(sei) + end end - if settings.security[:want_assertions_signed] && !(signed_elements.include? "Assertion") - return append_error("The Assertion of the Response is not signed and the SP requires it") - end + signed_elements << signed_element + end - true + unless signature_nodes.length < 3 && !signed_elements.empty? + return append_error("Found an unexpected number of Signature Element. SAML Response rejected") end - # Validates if the provided request_id match the inResponseTo value. - # If fails, the error is added to the errors array - # @return [Boolean] True if there is no request_id or it match, otherwise False if soft=True - # @raise [ValidationError] if soft == false and validation fails - # - def validate_in_response_to - return true unless options.key? :matches_request_id - return true if options[:matches_request_id].nil? - return true unless options[:matches_request_id] != in_response_to - - error_msg = "The InResponseTo of the Response: #{in_response_to}, does not match the ID of the AuthNRequest sent by the SP: #{options[:matches_request_id]}" - append_error(error_msg) + if settings.security[:want_assertions_signed] && !(signed_elements.include? "Assertion") + return append_error("The Assertion of the Response is not signed and the SP requires it") end - # Validates the Audience, (If the Audience match the Service Provider EntityID) - # If the response was initialized with the :skip_audience option, this validation is skipped, - # If fails, the error is added to the errors array - # @return [Boolean] True if there is an Audience Element that match the Service Provider EntityID, otherwise False if soft=True - # @raise [ValidationError] if soft == false and validation fails - # - def validate_audience - return true if options[:skip_audience] - return true if settings.sp_entity_id.nil? || settings.sp_entity_id.empty? - - if audiences.empty? - return true unless settings.security[:strict_audience_validation] - return append_error("Invalid Audiences. The element contained only empty elements. Expected audience #{settings.sp_entity_id}.") - end + true + end - unless audiences.include? settings.sp_entity_id - s = audiences.count > 1 ? 's' : '' - error_msg = "Invalid Audience#{s}. The audience#{s} #{audiences.join(',')}, did not match the expected audience #{settings.sp_entity_id}" - return append_error(error_msg) - end + # Validates if the provided request_id match the inResponseTo value. + # If fails, the error is added to the errors array + # @return [Boolean] True if there is no request_id or it match, otherwise False if soft=True + # @raise [ValidationError] if soft == false and validation fails + # + def validate_in_response_to + return true unless options.key? :matches_request_id + return true if options[:matches_request_id].nil? + return true unless options[:matches_request_id] != in_response_to - true + error_msg = "The InResponseTo of the Response: #{in_response_to}, does not match the ID of the AuthNRequest sent by the SP: #{options[:matches_request_id]}" + append_error(error_msg) + end + + # Validates the Audience, (If the Audience match the Service Provider EntityID) + # If the response was initialized with the :skip_audience option, this validation is skipped, + # If fails, the error is added to the errors array + # @return [Boolean] True if there is an Audience Element that match the Service Provider EntityID, otherwise False if soft=True + # @raise [ValidationError] if soft == false and validation fails + # + def validate_audience + return true if options[:skip_audience] + return true if settings.sp_entity_id.nil? || settings.sp_entity_id.empty? + + if audiences.empty? + return true unless settings.security[:strict_audience_validation] + return append_error("Invalid Audiences. The element contained only empty elements. Expected audience #{settings.sp_entity_id}.") end - # Validates the Destination, (If the SAML Response is received where expected). - # If the response was initialized with the :skip_destination option, this validation is skipped, - # If fails, the error is added to the errors array - # @return [Boolean] True if there is a Destination element that matches the Consumer Service URL, otherwise False - # - def validate_destination - return true if destination.nil? - return true if options[:skip_destination] - - if destination.empty? - error_msg = "The response has an empty Destination value" - return append_error(error_msg) - end + unless audiences.include? settings.sp_entity_id + s = audiences.count > 1 ? 's' : '' + error_msg = "Invalid Audience#{s}. The audience#{s} #{audiences.join(',')}, did not match the expected audience #{settings.sp_entity_id}" + return append_error(error_msg) + end - return true if settings.assertion_consumer_service_url.nil? || settings.assertion_consumer_service_url.empty? + true + end - unless OneLogin::RubySaml::Utils.uri_match?(destination, settings.assertion_consumer_service_url) - error_msg = "The response was received at #{destination} instead of #{settings.assertion_consumer_service_url}" - return append_error(error_msg) - end + # Validates the Destination, (If the SAML Response is received where expected). + # If the response was initialized with the :skip_destination option, this validation is skipped, + # If fails, the error is added to the errors array + # @return [Boolean] True if there is a Destination element that matches the Consumer Service URL, otherwise False + # + def validate_destination + return true if destination.nil? + return true if options[:skip_destination] - true + if destination.empty? + error_msg = "The response has an empty Destination value" + return append_error(error_msg) end - # Checks that the samlp:Response/saml:Assertion/saml:Conditions element exists and is unique. - # (If the response was initialized with the :skip_conditions option, this validation is skipped) - # If fails, the error is added to the errors array - # @return [Boolean] True if there is a conditions element and is unique - # - def validate_one_conditions - return true if options[:skip_conditions] - - conditions_nodes = xpath_from_signed_assertion('/a:Conditions') - unless conditions_nodes.size == 1 - error_msg = "The Assertion must include one Conditions element" - return append_error(error_msg) - end + return true if settings.assertion_consumer_service_url.nil? || settings.assertion_consumer_service_url.empty? - true + unless RubySaml::Utils.uri_match?(destination, settings.assertion_consumer_service_url) + error_msg = "The response was received at #{destination} instead of #{settings.assertion_consumer_service_url}" + return append_error(error_msg) end - # Checks that the samlp:Response/saml:Assertion/saml:AuthnStatement element exists and is unique. - # If fails, the error is added to the errors array - # @return [Boolean] True if there is a authnstatement element and is unique - # - def validate_one_authnstatement - return true if options[:skip_authnstatement] + true + end - authnstatement_nodes = xpath_from_signed_assertion('/a:AuthnStatement') - unless authnstatement_nodes.size == 1 - error_msg = "The Assertion must include one AuthnStatement element" - return append_error(error_msg) - end + # Checks that the samlp:Response/saml:Assertion/saml:Conditions element exists and is unique. + # (If the response was initialized with the :skip_conditions option, this validation is skipped) + # If fails, the error is added to the errors array + # @return [Boolean] True if there is a conditions element and is unique + # + def validate_one_conditions + return true if options[:skip_conditions] - true + conditions_nodes = xpath_from_signed_assertion('/a:Conditions') + unless conditions_nodes.size == 1 + error_msg = "The Assertion must include one Conditions element" + return append_error(error_msg) end - # Validates the Conditions. (If the response was initialized with the :skip_conditions option, this validation is skipped, - # If the response was initialized with the :allowed_clock_drift option, the timing validations are relaxed by the allowed_clock_drift value) - # @return [Boolean] True if satisfies the conditions, otherwise False if soft=True - # @raise [ValidationError] if soft == false and validation fails - # - def validate_conditions - return true if conditions.nil? - return true if options[:skip_conditions] + true + end + + # Checks that the samlp:Response/saml:Assertion/saml:AuthnStatement element exists and is unique. + # If fails, the error is added to the errors array + # @return [Boolean] True if there is a authnstatement element and is unique + # + def validate_one_authnstatement + return true if options[:skip_authnstatement] - now = Time.now.utc + authnstatement_nodes = xpath_from_signed_assertion('/a:AuthnStatement') + unless authnstatement_nodes.size == 1 + error_msg = "The Assertion must include one AuthnStatement element" + return append_error(error_msg) + end - if not_before && now < (not_before - allowed_clock_drift) - error_msg = "Current time is earlier than NotBefore condition (#{now} < #{not_before}#{" - #{allowed_clock_drift.ceil}s" if allowed_clock_drift > 0})" - return append_error(error_msg) - end + true + end - if not_on_or_after && now >= (not_on_or_after + allowed_clock_drift) - error_msg = "Current time is on or after NotOnOrAfter condition (#{now} >= #{not_on_or_after}#{" + #{allowed_clock_drift.ceil}s" if allowed_clock_drift > 0})" - return append_error(error_msg) - end + # Validates the Conditions. (If the response was initialized with the :skip_conditions option, this validation is skipped, + # If the response was initialized with the :allowed_clock_drift option, the timing validations are relaxed by the allowed_clock_drift value) + # @return [Boolean] True if satisfies the conditions, otherwise False if soft=True + # @raise [ValidationError] if soft == false and validation fails + # + def validate_conditions + return true if conditions.nil? + return true if options[:skip_conditions] - true + now = Time.now.utc + + if not_before && now < (not_before - allowed_clock_drift) + error_msg = "Current time is earlier than NotBefore condition (#{now} < #{not_before}#{" - #{allowed_clock_drift.ceil}s" if allowed_clock_drift > 0})" + return append_error(error_msg) end - # Validates the Issuer (Of the SAML Response and the SAML Assertion) - # @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the response is invalid or not) - # @return [Boolean] True if the Issuer matchs the IdP entityId, otherwise False if soft=True - # @raise [ValidationError] if soft == false and validation fails - # - def validate_issuer - return true if settings.idp_entity_id.nil? + if not_on_or_after && now >= (not_on_or_after + allowed_clock_drift) + error_msg = "Current time is on or after NotOnOrAfter condition (#{now} >= #{not_on_or_after}#{" + #{allowed_clock_drift.ceil}s" if allowed_clock_drift > 0})" + return append_error(error_msg) + end - begin - obtained_issuers = issuers - rescue ValidationError => e - return append_error(e.message) - end + true + end - obtained_issuers.each do |issuer| - unless OneLogin::RubySaml::Utils.uri_match?(issuer, settings.idp_entity_id) - error_msg = "Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>" - return append_error(error_msg) - end - end + # Validates the Issuer (Of the SAML Response and the SAML Assertion) + # @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the response is invalid or not) + # @return [Boolean] True if the Issuer matchs the IdP entityId, otherwise False if soft=True + # @raise [ValidationError] if soft == false and validation fails + # + def validate_issuer + return true if settings.idp_entity_id.nil? - true + begin + obtained_issuers = issuers + rescue ValidationError => e + return append_error(e.message) end - # Validates that the Session haven't expired (If the response was initialized with the :allowed_clock_drift option, - # this time validation is relaxed by the allowed_clock_drift value) - # If fails, the error is added to the errors array - # @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the response is invalid or not) - # @return [Boolean] True if the SessionNotOnOrAfter of the AuthnStatement is valid, otherwise (when expired) False if soft=True - # @raise [ValidationError] if soft == false and validation fails - # - def validate_session_expiration - return true if session_expires_at.nil? - - now = Time.now.utc - unless now < (session_expires_at + allowed_clock_drift) - error_msg = "The attributes have expired, based on the SessionNotOnOrAfter of the AuthnStatement of this Response" + obtained_issuers.each do |issuer| + unless RubySaml::Utils.uri_match?(issuer, settings.idp_entity_id) + error_msg = "Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>" return append_error(error_msg) end - - true end - # Validates if exists valid SubjectConfirmation (If the response was initialized with the :allowed_clock_drift option, - # timimg validation are relaxed by the allowed_clock_drift value. If the response was initialized with the - # :skip_subject_confirmation option, this validation is skipped) - # There is also an optional Recipient check - # If fails, the error is added to the errors array - # @return [Boolean] True if exists a valid SubjectConfirmation, otherwise False if soft=True - # @raise [ValidationError] if soft == false and validation fails - # - def validate_subject_confirmation - return true if options[:skip_subject_confirmation] - valid_subject_confirmation = false - - subject_confirmation_nodes = xpath_from_signed_assertion('/a:Subject/a:SubjectConfirmation') - - now = Time.now.utc - subject_confirmation_nodes.each do |subject_confirmation| - if subject_confirmation.attributes.include? "Method" and subject_confirmation.attributes['Method'] != 'urn:oasis:names:tc:SAML:2.0:cm:bearer' - next - end + true + end - confirmation_data_node = REXML::XPath.first( - subject_confirmation, - 'a:SubjectConfirmationData', - { "a" => ASSERTION } - ) + # Validates that the Session haven't expired (If the response was initialized with the :allowed_clock_drift option, + # this time validation is relaxed by the allowed_clock_drift value) + # If fails, the error is added to the errors array + # @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the response is invalid or not) + # @return [Boolean] True if the SessionNotOnOrAfter of the AuthnStatement is valid, otherwise (when expired) False if soft=True + # @raise [ValidationError] if soft == false and validation fails + # + def validate_session_expiration + return true if session_expires_at.nil? + + now = Time.now.utc + unless now < (session_expires_at + allowed_clock_drift) + error_msg = "The attributes have expired, based on the SessionNotOnOrAfter of the AuthnStatement of this Response" + return append_error(error_msg) + end - next unless confirmation_data_node + true + end - attrs = confirmation_data_node.attributes - next if (attrs.include? "InResponseTo" and attrs['InResponseTo'] != in_response_to) || - (attrs.include? "NotBefore" and now < (parse_time(confirmation_data_node, "NotBefore") - allowed_clock_drift)) || - (attrs.include? "NotOnOrAfter" and now >= (parse_time(confirmation_data_node, "NotOnOrAfter") + allowed_clock_drift)) || - (attrs.include? "Recipient" and !options[:skip_recipient_check] and settings and attrs['Recipient'] != settings.assertion_consumer_service_url) + # Validates if exists valid SubjectConfirmation (If the response was initialized with the :allowed_clock_drift option, + # timimg validation are relaxed by the allowed_clock_drift value. If the response was initialized with the + # :skip_subject_confirmation option, this validation is skipped) + # There is also an optional Recipient check + # If fails, the error is added to the errors array + # @return [Boolean] True if exists a valid SubjectConfirmation, otherwise False if soft=True + # @raise [ValidationError] if soft == false and validation fails + # + def validate_subject_confirmation + return true if options[:skip_subject_confirmation] + valid_subject_confirmation = false - valid_subject_confirmation = true - break - end + subject_confirmation_nodes = xpath_from_signed_assertion('/a:Subject/a:SubjectConfirmation') - unless valid_subject_confirmation - error_msg = "A valid SubjectConfirmation was not found on this Response" - return append_error(error_msg) + now = Time.now.utc + subject_confirmation_nodes.each do |subject_confirmation| + if subject_confirmation.attributes.include? "Method" and subject_confirmation.attributes['Method'] != 'urn:oasis:names:tc:SAML:2.0:cm:bearer' + next end - true + confirmation_data_node = REXML::XPath.first( + subject_confirmation, + 'a:SubjectConfirmationData', + { "a" => ASSERTION } + ) + + next unless confirmation_data_node + + attrs = confirmation_data_node.attributes + next if (attrs.include? "InResponseTo" and attrs['InResponseTo'] != in_response_to) || + (attrs.include? "NotBefore" and now < (parse_time(confirmation_data_node, "NotBefore") - allowed_clock_drift)) || + (attrs.include? "NotOnOrAfter" and now >= (parse_time(confirmation_data_node, "NotOnOrAfter") + allowed_clock_drift)) || + (attrs.include? "Recipient" and !options[:skip_recipient_check] and settings and attrs['Recipient'] != settings.assertion_consumer_service_url) + + valid_subject_confirmation = true + break end - # Validates the NameID element - def validate_name_id - if name_id_node.nil? - if settings.security[:want_name_id] - return append_error("No NameID element found in the assertion of the Response") - end - else - if name_id.nil? || name_id.empty? - return append_error("An empty NameID value found") - end + unless valid_subject_confirmation + error_msg = "A valid SubjectConfirmation was not found on this Response" + return append_error(error_msg) + end - if !(settings.sp_entity_id.nil? || settings.sp_entity_id.empty? || name_id_spnamequalifier.nil? || name_id_spnamequalifier.empty?) && (name_id_spnamequalifier != settings.sp_entity_id) - return append_error("The SPNameQualifier value mistmatch the SP entityID value.") - end + true + end + + # Validates the NameID element + def validate_name_id + if name_id_node.nil? + if settings.security[:want_name_id] + return append_error("No NameID element found in the assertion of the Response") + end + else + if name_id.nil? || name_id.empty? + return append_error("An empty NameID value found") end - true + if !(settings.sp_entity_id.nil? || settings.sp_entity_id.empty? || name_id_spnamequalifier.nil? || name_id_spnamequalifier.empty?) && (name_id_spnamequalifier != settings.sp_entity_id) + return append_error("The SPNameQualifier value mistmatch the SP entityID value.") + end end - # Validates the Signature - # @return [Boolean] True if not contains a Signature or if the Signature is valid, otherwise False if soft=True - # @raise [ValidationError] if soft == false and validation fails - # - def validate_signature - error_msg = "Invalid Signature on SAML Response" + true + end - # If the response contains the signature, and the assertion was encrypted, validate the original SAML Response - # otherwise, review if the decrypted assertion contains a signature + # Validates the Signature + # @return [Boolean] True if not contains a Signature or if the Signature is valid, otherwise False if soft=True + # @raise [ValidationError] if soft == false and validation fails + # + def validate_signature + error_msg = "Invalid Signature on SAML Response" + + # If the response contains the signature, and the assertion was encrypted, validate the original SAML Response + # otherwise, review if the decrypted assertion contains a signature + sig_elements = REXML::XPath.match( + document, + "/p:Response[@ID=$id]/ds:Signature", + { "p" => PROTOCOL, "ds" => DSIG }, + { 'id' => document.signed_element_id } + ) + + use_original = sig_elements.size == 1 || decrypted_document.nil? + doc = use_original ? document : decrypted_document + + # Check signature nodes + if sig_elements.nil? || sig_elements.empty? sig_elements = REXML::XPath.match( - document, - "/p:Response[@ID=$id]/ds:Signature", - { "p" => PROTOCOL, "ds" => DSIG }, - { 'id' => document.signed_element_id } + doc, + "/p:Response/a:Assertion[@ID=$id]/ds:Signature", + {"p" => PROTOCOL, "a" => ASSERTION, "ds"=>DSIG}, + { 'id' => doc.signed_element_id } ) + end - use_original = sig_elements.size == 1 || decrypted_document.nil? - doc = use_original ? document : decrypted_document - - # Check signature nodes - if sig_elements.nil? || sig_elements.empty? - sig_elements = REXML::XPath.match( - doc, - "/p:Response/a:Assertion[@ID=$id]/ds:Signature", - {"p" => PROTOCOL, "a" => ASSERTION, "ds"=>DSIG}, - { 'id' => doc.signed_element_id } - ) + if sig_elements.size != 1 + if sig_elements.empty? + append_error("Signed element id ##{doc.signed_element_id} is not found") + else + append_error("Signed element id ##{doc.signed_element_id} is found more than once") end + return append_error(error_msg) + end - if sig_elements.size != 1 - if sig_elements.empty? - append_error("Signed element id ##{doc.signed_element_id} is not found") - else - append_error("Signed element id ##{doc.signed_element_id} is found more than once") + old_errors = @errors.clone + + idp_certs = settings.get_idp_cert_multi + if idp_certs.nil? || idp_certs[:signing].empty? + opts = {} + opts[:fingerprint_alg] = settings.idp_cert_fingerprint_algorithm + idp_cert = settings.get_idp_cert + fingerprint = settings.get_fingerprint + opts[:cert] = idp_cert + + if fingerprint && doc.validate_document(fingerprint, @soft, opts) + if settings.security[:check_idp_cert_expiration] && RubySaml::Utils.is_cert_expired(idp_cert) + error_msg = "IdP x509 certificate expired" + return append_error(error_msg) end + else return append_error(error_msg) end + else + valid = false + expired = false + idp_certs[:signing].each do |idp_cert| + valid = doc.validate_document_with_cert(idp_cert, true) + next unless valid + if settings.security[:check_idp_cert_expiration] && RubySaml::Utils.is_cert_expired(idp_cert) + expired = true + end - old_errors = @errors.clone + # At least one certificate is valid, restore the old accumulated errors + @errors = old_errors + break + end + if expired + error_msg = "IdP x509 certificate expired" + return append_error(error_msg) + end + unless valid + # Remove duplicated errors + @errors = @errors.uniq + return append_error(error_msg) + end + end - idp_certs = settings.get_idp_cert_multi - if idp_certs.nil? || idp_certs[:signing].empty? - opts = {} - opts[:fingerprint_alg] = settings.idp_cert_fingerprint_algorithm - idp_cert = settings.get_idp_cert - fingerprint = settings.get_fingerprint - opts[:cert] = idp_cert + true + end - if fingerprint && doc.validate_document(fingerprint, @soft, opts) - if settings.security[:check_idp_cert_expiration] && OneLogin::RubySaml::Utils.is_cert_expired(idp_cert) - error_msg = "IdP x509 certificate expired" - return append_error(error_msg) - end + def name_id_node + @name_id_node ||= + begin + encrypted_node = xpath_first_from_signed_assertion('/a:Subject/a:EncryptedID') + if encrypted_node + decrypt_nameid(encrypted_node) else - return append_error(error_msg) - end - else - valid = false - expired = false - idp_certs[:signing].each do |idp_cert| - valid = doc.validate_document_with_cert(idp_cert, true) - next unless valid - if settings.security[:check_idp_cert_expiration] && OneLogin::RubySaml::Utils.is_cert_expired(idp_cert) - expired = true - end - - # At least one certificate is valid, restore the old accumulated errors - @errors = old_errors - break - end - if expired - error_msg = "IdP x509 certificate expired" - return append_error(error_msg) - end - unless valid - # Remove duplicated errors - @errors = @errors.uniq - return append_error(error_msg) + xpath_first_from_signed_assertion('/a:Subject/a:NameID') end end + end - true - end + # Extracts the first appearance that matchs the subelt (pattern) + # Search on any Assertion that is signed, or has a Response parent signed + # @param subelt [String] The XPath pattern + # @return [REXML::Element | nil] If any matches, return the Element + # + def xpath_first_from_signed_assertion(subelt=nil) + doc = decrypted_document.nil? ? document : decrypted_document + node = REXML::XPath.first( + doc, + "/p:Response/a:Assertion[@ID=$id]#{subelt}", + { "p" => PROTOCOL, "a" => ASSERTION }, + { 'id' => doc.signed_element_id } + ) + node ||= REXML::XPath.first( + doc, + "/p:Response[@ID=$id]/a:Assertion#{subelt}", + { "p" => PROTOCOL, "a" => ASSERTION }, + { 'id' => doc.signed_element_id } + ) + node + end - def name_id_node - @name_id_node ||= - begin - encrypted_node = xpath_first_from_signed_assertion('/a:Subject/a:EncryptedID') - if encrypted_node - decrypt_nameid(encrypted_node) - else - xpath_first_from_signed_assertion('/a:Subject/a:NameID') - end - end - end + # Extracts all the appearances that matchs the subelt (pattern) + # Search on any Assertion that is signed, or has a Response parent signed + # @param subelt [String] The XPath pattern + # @return [Array of REXML::Element] Return all matches + # + def xpath_from_signed_assertion(subelt=nil) + doc = decrypted_document.nil? ? document : decrypted_document + node = REXML::XPath.match( + doc, + "/p:Response/a:Assertion[@ID=$id]#{subelt}", + { "p" => PROTOCOL, "a" => ASSERTION }, + { 'id' => doc.signed_element_id } + ) + node.concat(REXML::XPath.match( + doc, + "/p:Response[@ID=$id]/a:Assertion#{subelt}", + { "p" => PROTOCOL, "a" => ASSERTION }, + { 'id' => doc.signed_element_id } + )) + end - # Extracts the first appearance that matchs the subelt (pattern) - # Search on any Assertion that is signed, or has a Response parent signed - # @param subelt [String] The XPath pattern - # @return [REXML::Element | nil] If any matches, return the Element - # - def xpath_first_from_signed_assertion(subelt=nil) - doc = decrypted_document.nil? ? document : decrypted_document - node = REXML::XPath.first( - doc, - "/p:Response/a:Assertion[@ID=$id]#{subelt}", - { "p" => PROTOCOL, "a" => ASSERTION }, - { 'id' => doc.signed_element_id } - ) - node ||= REXML::XPath.first( - doc, - "/p:Response[@ID=$id]/a:Assertion#{subelt}", - { "p" => PROTOCOL, "a" => ASSERTION }, - { 'id' => doc.signed_element_id } - ) - node + # Generates the decrypted_document + # @return [XMLSecurity::SignedDocument] The SAML Response with the assertion decrypted + # + def generate_decrypted_document + if settings.nil? || settings.get_sp_decryption_keys.empty? + raise ValidationError.new('An EncryptedAssertion found and no SP private key found on the settings to decrypt it. Be sure you provided the :settings parameter at the initialize method') end - # Extracts all the appearances that matchs the subelt (pattern) - # Search on any Assertion that is signed, or has a Response parent signed - # @param subelt [String] The XPath pattern - # @return [Array of REXML::Element] Return all matches - # - def xpath_from_signed_assertion(subelt=nil) - doc = decrypted_document.nil? ? document : decrypted_document - node = REXML::XPath.match( - doc, - "/p:Response/a:Assertion[@ID=$id]#{subelt}", - { "p" => PROTOCOL, "a" => ASSERTION }, - { 'id' => doc.signed_element_id } - ) - node.concat(REXML::XPath.match( - doc, - "/p:Response[@ID=$id]/a:Assertion#{subelt}", - { "p" => PROTOCOL, "a" => ASSERTION }, - { 'id' => doc.signed_element_id } - )) - end + document_copy = Marshal.load(Marshal.dump(document)) - # Generates the decrypted_document - # @return [XMLSecurity::SignedDocument] The SAML Response with the assertion decrypted - # - def generate_decrypted_document - if settings.nil? || settings.get_sp_decryption_keys.empty? - raise ValidationError.new('An EncryptedAssertion found and no SP private key found on the settings to decrypt it. Be sure you provided the :settings parameter at the initialize method') - end + decrypt_assertion_from_document(document_copy) + end - document_copy = Marshal.load(Marshal.dump(document)) + # Obtains a SAML Response with the EncryptedAssertion element decrypted + # @param document_copy [XMLSecurity::SignedDocument] A copy of the original SAML Response with the encrypted assertion + # @return [XMLSecurity::SignedDocument] The SAML Response with the assertion decrypted + # + def decrypt_assertion_from_document(document_copy) + response_node = REXML::XPath.first( + document_copy, + "/p:Response/", + { "p" => PROTOCOL } + ) + encrypted_assertion_node = REXML::XPath.first( + document_copy, + "(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)", + { "p" => PROTOCOL, "a" => ASSERTION } + ) + response_node.add(decrypt_assertion(encrypted_assertion_node)) + encrypted_assertion_node.remove + XMLSecurity::SignedDocument.new(response_node.to_s) + end - decrypt_assertion_from_document(document_copy) - end + # Decrypts an EncryptedAssertion element + # @param encrypted_assertion_node [REXML::Element] The EncryptedAssertion element + # @return [REXML::Document] The decrypted EncryptedAssertion element + # + def decrypt_assertion(encrypted_assertion_node) + decrypt_element(encrypted_assertion_node, %r{(.*)}m) + end - # Obtains a SAML Response with the EncryptedAssertion element decrypted - # @param document_copy [XMLSecurity::SignedDocument] A copy of the original SAML Response with the encrypted assertion - # @return [XMLSecurity::SignedDocument] The SAML Response with the assertion decrypted - # - def decrypt_assertion_from_document(document_copy) - response_node = REXML::XPath.first( - document_copy, - "/p:Response/", - { "p" => PROTOCOL } - ) - encrypted_assertion_node = REXML::XPath.first( - document_copy, - "(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)", - { "p" => PROTOCOL, "a" => ASSERTION } - ) - response_node.add(decrypt_assertion(encrypted_assertion_node)) - encrypted_assertion_node.remove - XMLSecurity::SignedDocument.new(response_node.to_s) - end + # Decrypts an EncryptedID element + # @param encrypted_id_node [REXML::Element] The EncryptedID element + # @return [REXML::Document] The decrypted EncrypedtID element + # + def decrypt_nameid(encrypted_id_node) + decrypt_element(encrypted_id_node, /(.*<\/(\w+:)?NameID>)/m) + end - # Decrypts an EncryptedAssertion element - # @param encrypted_assertion_node [REXML::Element] The EncryptedAssertion element - # @return [REXML::Document] The decrypted EncryptedAssertion element - # - def decrypt_assertion(encrypted_assertion_node) - decrypt_element(encrypted_assertion_node, %r{(.*)}m) - end + # Decrypts an EncryptedAttribute element + # @param encrypted_attribute_node [REXML::Element] The EncryptedAttribute element + # @return [REXML::Document] The decrypted EncryptedAttribute element + # + def decrypt_attribute(encrypted_attribute_node) + decrypt_element(encrypted_attribute_node, /(.*<\/(\w+:)?Attribute>)/m) + end - # Decrypts an EncryptedID element - # @param encrypted_id_node [REXML::Element] The EncryptedID element - # @return [REXML::Document] The decrypted EncrypedtID element - # - def decrypt_nameid(encrypted_id_node) - decrypt_element(encrypted_id_node, /(.*<\/(\w+:)?NameID>)/m) + # Decrypt an element + # @param encrypt_node [REXML::Element] The encrypted element + # @param regexp [Regexp] The regular expression to extract the decrypted data + # @return [REXML::Document] The decrypted element + # + def decrypt_element(encrypt_node, regexp) + if settings.nil? || settings.get_sp_decryption_keys.empty? + raise ValidationError.new('An ' + encrypt_node.name + ' found and no SP private key found on the settings to decrypt it') end - # Decrypts an EncryptedAttribute element - # @param encrypted_attribute_node [REXML::Element] The EncryptedAttribute element - # @return [REXML::Document] The decrypted EncryptedAttribute element - # - def decrypt_attribute(encrypted_attribute_node) - decrypt_element(encrypted_attribute_node, /(.*<\/(\w+:)?Attribute>)/m) + if encrypt_node.name == 'EncryptedAttribute' + node_header = '' + else + node_header = '' end - # Decrypt an element - # @param encrypt_node [REXML::Element] The encrypted element - # @param regexp [Regexp] The regular expression to extract the decrypted data - # @return [REXML::Document] The decrypted element - # - def decrypt_element(encrypt_node, regexp) - if settings.nil? || settings.get_sp_decryption_keys.empty? - raise ValidationError.new('An ' + encrypt_node.name + ' found and no SP private key found on the settings to decrypt it') - end - - if encrypt_node.name == 'EncryptedAttribute' - node_header = '' - else - node_header = '' - end - - elem_plaintext = OneLogin::RubySaml::Utils.decrypt_multi(encrypt_node, settings.get_sp_decryption_keys) + elem_plaintext = RubySaml::Utils.decrypt_multi(encrypt_node, settings.get_sp_decryption_keys) - # If we get some problematic noise in the plaintext after decrypting. - # This quick regexp parse will grab only the Element and discard the noise. - elem_plaintext = elem_plaintext.match(regexp)[0] + # If we get some problematic noise in the plaintext after decrypting. + # This quick regexp parse will grab only the Element and discard the noise. + elem_plaintext = elem_plaintext.match(regexp)[0] - # To avoid namespace errors if saml namespace is not defined - # create a parent node first with the namespace defined - elem_plaintext = "#{node_header}#{elem_plaintext}" - doc = REXML::Document.new(elem_plaintext) - doc.root[0] - end + # To avoid namespace errors if saml namespace is not defined + # create a parent node first with the namespace defined + elem_plaintext = "#{node_header}#{elem_plaintext}" + doc = REXML::Document.new(elem_plaintext) + doc.root[0] + end - # Parse the attribute of a given node in Time format - # @param node [REXML:Element] The node - # @param attribute [String] The attribute name - # @return [Time|nil] The parsed value - # - def parse_time(node, attribute) - return unless node && node.attributes[attribute] - Time.parse(node.attributes[attribute]) - end + # Parse the attribute of a given node in Time format + # @param node [REXML:Element] The node + # @param attribute [String] The attribute name + # @return [Time|nil] The parsed value + # + def parse_time(node, attribute) + return unless node && node.attributes[attribute] + Time.parse(node.attributes[attribute]) end end end diff --git a/lib/onelogin/ruby-saml/saml_message.rb b/lib/onelogin/ruby-saml/saml_message.rb index 7677925ae..2c1c7cd53 100644 --- a/lib/onelogin/ruby-saml/saml_message.rb +++ b/lib/onelogin/ruby-saml/saml_message.rb @@ -9,154 +9,152 @@ require "onelogin/ruby-saml/error_handling" # Only supports SAML 2.0 -module OneLogin - module RubySaml +module RubySaml - # SAML2 Message + # SAML2 Message + # + class SamlMessage + include REXML + + ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion" + PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol" + + BASE64_FORMAT = %r(\A([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?\Z) + @@mutex = Mutex.new + + # @return [Nokogiri::XML::Schema] Gets the schema object of the SAML 2.0 Protocol schema # - class SamlMessage - include REXML - - ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion" - PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol" - - BASE64_FORMAT = %r(\A([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?\Z) - @@mutex = Mutex.new - - # @return [Nokogiri::XML::Schema] Gets the schema object of the SAML 2.0 Protocol schema - # - def self.schema - @@mutex.synchronize do - Dir.chdir(File.expand_path('../../schemas', __dir__)) do - ::Nokogiri::XML::Schema(File.read("saml-schema-protocol-2.0.xsd")) - end + def self.schema + @@mutex.synchronize do + Dir.chdir(File.expand_path('../../schemas', __dir__)) do + ::Nokogiri::XML::Schema(File.read("saml-schema-protocol-2.0.xsd")) end end + end - # @return [String|nil] Gets the Version attribute from the SAML Message if exists. - # - def version(document) - @version ||= begin - node = REXML::XPath.first( - document, - "/p:AuthnRequest | /p:Response | /p:LogoutResponse | /p:LogoutRequest", - { "p" => PROTOCOL } - ) - node.nil? ? nil : node.attributes['Version'] - end + # @return [String|nil] Gets the Version attribute from the SAML Message if exists. + # + def version(document) + @version ||= begin + node = REXML::XPath.first( + document, + "/p:AuthnRequest | /p:Response | /p:LogoutResponse | /p:LogoutRequest", + { "p" => PROTOCOL } + ) + node.nil? ? nil : node.attributes['Version'] end + end - # @return [String|nil] Gets the ID attribute from the SAML Message if exists. - # - def id(document) - @id ||= begin - node = REXML::XPath.first( - document, - "/p:AuthnRequest | /p:Response | /p:LogoutResponse | /p:LogoutRequest", - { "p" => PROTOCOL } - ) - node.nil? ? nil : node.attributes['ID'] - end + # @return [String|nil] Gets the ID attribute from the SAML Message if exists. + # + def id(document) + @id ||= begin + node = REXML::XPath.first( + document, + "/p:AuthnRequest | /p:Response | /p:LogoutResponse | /p:LogoutRequest", + { "p" => PROTOCOL } + ) + node.nil? ? nil : node.attributes['ID'] end + end - # Validates the SAML Message against the specified schema. - # @param document [REXML::Document] The message that will be validated - # @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the message is invalid or not) - # @return [Boolean] True if the XML is valid, otherwise False, if soft=True - # @raise [ValidationError] if soft == false and validation fails - # - def valid_saml?(document, soft = true) - begin - xml = Nokogiri::XML(document.to_s) do |config| - config.options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS - end - rescue StandardError => error - return false if soft - raise ValidationError.new("XML load failed: #{error.message}") + # Validates the SAML Message against the specified schema. + # @param document [REXML::Document] The message that will be validated + # @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the message is invalid or not) + # @return [Boolean] True if the XML is valid, otherwise False, if soft=True + # @raise [ValidationError] if soft == false and validation fails + # + def valid_saml?(document, soft = true) + begin + xml = Nokogiri::XML(document.to_s) do |config| + config.options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS end + rescue StandardError => error + return false if soft + raise ValidationError.new("XML load failed: #{error.message}") + end - SamlMessage.schema.validate(xml).map do |schema_error| - return false if soft - raise ValidationError.new("#{schema_error.message}\n\n#{xml}") - end + SamlMessage.schema.validate(xml).map do |schema_error| + return false if soft + raise ValidationError.new("#{schema_error.message}\n\n#{xml}") end + end - private + private - # Base64 decode and try also to inflate a SAML Message - # @param saml [String] The deflated and encoded SAML Message - # @return [String] The plain SAML Message - # - def decode_raw_saml(saml, settings = nil) - return saml unless base64_encoded?(saml) + # Base64 decode and try also to inflate a SAML Message + # @param saml [String] The deflated and encoded SAML Message + # @return [String] The plain SAML Message + # + def decode_raw_saml(saml, settings = nil) + return saml unless base64_encoded?(saml) - settings = OneLogin::RubySaml::Settings.new if settings.nil? - if saml.bytesize > settings.message_max_bytesize - raise ValidationError.new("Encoded SAML Message exceeds " + settings.message_max_bytesize.to_s + " bytes, so was rejected") - end + settings = RubySaml::Settings.new if settings.nil? + if saml.bytesize > settings.message_max_bytesize + raise ValidationError.new("Encoded SAML Message exceeds " + settings.message_max_bytesize.to_s + " bytes, so was rejected") + end - decoded = decode(saml) - begin - inflate(decoded) - rescue StandardError - decoded - end + decoded = decode(saml) + begin + inflate(decoded) + rescue StandardError + decoded end + end - # Deflate, base64 encode and url-encode a SAML Message (To be used in the HTTP-redirect binding) - # @param saml [String] The plain SAML Message - # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings - # @return [String] The deflated and encoded SAML Message (encoded if the compression is requested) - # - def encode_raw_saml(saml, settings) - saml = deflate(saml) if settings.compress_request + # Deflate, base64 encode and url-encode a SAML Message (To be used in the HTTP-redirect binding) + # @param saml [String] The plain SAML Message + # @param settings [RubySaml::Settings|nil] Toolkit settings + # @return [String] The deflated and encoded SAML Message (encoded if the compression is requested) + # + def encode_raw_saml(saml, settings) + saml = deflate(saml) if settings.compress_request - CGI.escape(encode(saml)) - end + CGI.escape(encode(saml)) + end - # Base 64 decode method - # @param string [String] The string message - # @return [String] The decoded string - # - def decode(string) - Base64.decode64(string) - end + # Base 64 decode method + # @param string [String] The string message + # @return [String] The decoded string + # + def decode(string) + Base64.decode64(string) + end - # Base 64 encode method - # @param string [String] The string - # @return [String] The encoded string - # - def encode(string) - if Base64.respond_to?(:strict_encode64) - Base64.strict_encode64(string) - else - Base64.encode64(string).gsub(/\n/, "") - end + # Base 64 encode method + # @param string [String] The string + # @return [String] The encoded string + # + def encode(string) + if Base64.respond_to?(:strict_encode64) + Base64.strict_encode64(string) + else + Base64.encode64(string).gsub(/\n/, "") end + end - # Check if a string is base64 encoded - # @param string [String] string to check the encoding of - # @return [true, false] whether or not the string is base64 encoded - # - def base64_encoded?(string) - !!string.gsub(/[\r\n]|\\r|\\n|\s/, "").match(BASE64_FORMAT) - end + # Check if a string is base64 encoded + # @param string [String] string to check the encoding of + # @return [true, false] whether or not the string is base64 encoded + # + def base64_encoded?(string) + !!string.gsub(/[\r\n]|\\r|\\n|\s/, "").match(BASE64_FORMAT) + end - # Inflate method - # @param deflated [String] The string - # @return [String] The inflated string - # - def inflate(deflated) - Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(deflated) - end + # Inflate method + # @param deflated [String] The string + # @return [String] The inflated string + # + def inflate(deflated) + Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(deflated) + end - # Deflate method - # @param inflated [String] The string - # @return [String] The deflated string - # - def deflate(inflated) - Zlib::Deflate.deflate(inflated, 9)[2..-5] - end + # Deflate method + # @param inflated [String] The string + # @return [String] The deflated string + # + def deflate(inflated) + Zlib::Deflate.deflate(inflated, 9)[2..-5] end end end diff --git a/lib/onelogin/ruby-saml/setting_error.rb b/lib/onelogin/ruby-saml/setting_error.rb index d642d43c2..879434968 100644 --- a/lib/onelogin/ruby-saml/setting_error.rb +++ b/lib/onelogin/ruby-saml/setting_error.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -module OneLogin - module RubySaml - class SettingError < StandardError - end +module RubySaml + class SettingError < StandardError end -end \ No newline at end of file +end diff --git a/lib/onelogin/ruby-saml/settings.rb b/lib/onelogin/ruby-saml/settings.rb index 935a0e562..558fdba9f 100644 --- a/lib/onelogin/ruby-saml/settings.rb +++ b/lib/onelogin/ruby-saml/settings.rb @@ -6,380 +6,377 @@ require "onelogin/ruby-saml/validation_error" # Only supports SAML 2.0 -module OneLogin - module RubySaml - - # SAML2 Toolkit Settings - # - class Settings - def initialize(overrides = {}, keep_security_attributes = false) - if keep_security_attributes - security_attributes = overrides.delete(:security) || {} - config = DEFAULTS.merge(overrides) - config[:security] = DEFAULTS[:security].merge(security_attributes) - else - config = DEFAULTS.merge(overrides) - end +module RubySaml + + # SAML2 Toolkit Settings + # + class Settings + def initialize(overrides = {}, keep_security_attributes = false) + if keep_security_attributes + security_attributes = overrides.delete(:security) || {} + config = DEFAULTS.merge(overrides) + config[:security] = DEFAULTS[:security].merge(security_attributes) + else + config = DEFAULTS.merge(overrides) + end - config.each do |k,v| - acc = "#{k}=".to_sym - if respond_to? acc - value = v.is_a?(Hash) ? v.dup : v - send(acc, value) - end + config.each do |k,v| + acc = "#{k}=".to_sym + if respond_to? acc + value = v.is_a?(Hash) ? v.dup : v + send(acc, value) end - @attribute_consuming_service = AttributeService.new end + @attribute_consuming_service = AttributeService.new + end - # IdP Data - attr_accessor :idp_entity_id - attr_writer :idp_sso_service_url - attr_writer :idp_slo_service_url - attr_accessor :idp_slo_response_service_url - attr_accessor :idp_cert - attr_accessor :idp_cert_fingerprint - attr_accessor :idp_cert_fingerprint_algorithm - attr_accessor :idp_cert_multi - attr_accessor :idp_attribute_names - attr_accessor :idp_name_qualifier - attr_accessor :valid_until - # SP Data - attr_writer :sp_entity_id - attr_accessor :assertion_consumer_service_url - attr_reader :assertion_consumer_service_binding - attr_writer :single_logout_service_url - attr_accessor :sp_name_qualifier - attr_accessor :name_identifier_format - attr_accessor :name_identifier_value - attr_accessor :name_identifier_value_requested - attr_accessor :sessionindex - attr_accessor :compress_request - attr_accessor :compress_response - attr_accessor :double_quote_xml_attribute_values - attr_accessor :message_max_bytesize - attr_accessor :passive - attr_reader :protocol_binding - attr_accessor :attributes_index - attr_accessor :force_authn - attr_accessor :certificate - attr_accessor :private_key - attr_accessor :sp_cert_multi - attr_accessor :authn_context - attr_accessor :authn_context_comparison - attr_accessor :authn_context_decl_ref - attr_reader :attribute_consuming_service - # Work-flow - attr_accessor :security - attr_accessor :soft - # Deprecated - attr_accessor :certificate_new - attr_accessor :assertion_consumer_logout_service_url - attr_reader :assertion_consumer_logout_service_binding - attr_accessor :issuer - attr_accessor :idp_sso_target_url - attr_accessor :idp_slo_target_url - - # @return [String] IdP Single Sign On Service URL - # - def idp_sso_service_url - @idp_sso_service_url || @idp_sso_target_url - end + # IdP Data + attr_accessor :idp_entity_id + attr_writer :idp_sso_service_url + attr_writer :idp_slo_service_url + attr_accessor :idp_slo_response_service_url + attr_accessor :idp_cert + attr_accessor :idp_cert_fingerprint + attr_accessor :idp_cert_fingerprint_algorithm + attr_accessor :idp_cert_multi + attr_accessor :idp_attribute_names + attr_accessor :idp_name_qualifier + attr_accessor :valid_until + # SP Data + attr_writer :sp_entity_id + attr_accessor :assertion_consumer_service_url + attr_reader :assertion_consumer_service_binding + attr_writer :single_logout_service_url + attr_accessor :sp_name_qualifier + attr_accessor :name_identifier_format + attr_accessor :name_identifier_value + attr_accessor :name_identifier_value_requested + attr_accessor :sessionindex + attr_accessor :compress_request + attr_accessor :compress_response + attr_accessor :double_quote_xml_attribute_values + attr_accessor :message_max_bytesize + attr_accessor :passive + attr_reader :protocol_binding + attr_accessor :attributes_index + attr_accessor :force_authn + attr_accessor :certificate + attr_accessor :private_key + attr_accessor :sp_cert_multi + attr_accessor :authn_context + attr_accessor :authn_context_comparison + attr_accessor :authn_context_decl_ref + attr_reader :attribute_consuming_service + # Work-flow + attr_accessor :security + attr_accessor :soft + # Deprecated + attr_accessor :certificate_new + attr_accessor :assertion_consumer_logout_service_url + attr_reader :assertion_consumer_logout_service_binding + attr_accessor :issuer + attr_accessor :idp_sso_target_url + attr_accessor :idp_slo_target_url + + # @return [String] IdP Single Sign On Service URL + # + def idp_sso_service_url + @idp_sso_service_url || @idp_sso_target_url + end - # @return [String] IdP Single Logout Service URL - # - def idp_slo_service_url - @idp_slo_service_url || @idp_slo_target_url - end + # @return [String] IdP Single Logout Service URL + # + def idp_slo_service_url + @idp_slo_service_url || @idp_slo_target_url + end - # @return [String] IdP Single Sign On Service Binding - # - def idp_sso_service_binding - @idp_sso_service_binding || idp_binding_from_embed_sign - end + # @return [String] IdP Single Sign On Service Binding + # + def idp_sso_service_binding + @idp_sso_service_binding || idp_binding_from_embed_sign + end - # Setter for IdP Single Sign On Service Binding - # @param value [String, Symbol]. - # - def idp_sso_service_binding=(value) - @idp_sso_service_binding = get_binding(value) - end + # Setter for IdP Single Sign On Service Binding + # @param value [String, Symbol]. + # + def idp_sso_service_binding=(value) + @idp_sso_service_binding = get_binding(value) + end - # @return [String] IdP Single Logout Service Binding - # - def idp_slo_service_binding - @idp_slo_service_binding || idp_binding_from_embed_sign - end + # @return [String] IdP Single Logout Service Binding + # + def idp_slo_service_binding + @idp_slo_service_binding || idp_binding_from_embed_sign + end - # Setter for IdP Single Logout Service Binding - # @param value [String, Symbol]. - # - def idp_slo_service_binding=(value) - @idp_slo_service_binding = get_binding(value) - end + # Setter for IdP Single Logout Service Binding + # @param value [String, Symbol]. + # + def idp_slo_service_binding=(value) + @idp_slo_service_binding = get_binding(value) + end - # @return [String] SP Entity ID - # - def sp_entity_id - @sp_entity_id || @issuer - end + # @return [String] SP Entity ID + # + def sp_entity_id + @sp_entity_id || @issuer + end - # Setter for SP Protocol Binding - # @param value [String, Symbol]. - # - def protocol_binding=(value) - @protocol_binding = get_binding(value) - end + # Setter for SP Protocol Binding + # @param value [String, Symbol]. + # + def protocol_binding=(value) + @protocol_binding = get_binding(value) + end - # Setter for SP Assertion Consumer Service Binding - # @param value [String, Symbol]. - # - def assertion_consumer_service_binding=(value) - @assertion_consumer_service_binding = get_binding(value) - end + # Setter for SP Assertion Consumer Service Binding + # @param value [String, Symbol]. + # + def assertion_consumer_service_binding=(value) + @assertion_consumer_service_binding = get_binding(value) + end - # @return [String] Single Logout Service URL. - # - def single_logout_service_url - @single_logout_service_url || @assertion_consumer_logout_service_url - end + # @return [String] Single Logout Service URL. + # + def single_logout_service_url + @single_logout_service_url || @assertion_consumer_logout_service_url + end - # @return [String] Single Logout Service Binding. - # - def single_logout_service_binding - @single_logout_service_binding || @assertion_consumer_logout_service_binding - end + # @return [String] Single Logout Service Binding. + # + def single_logout_service_binding + @single_logout_service_binding || @assertion_consumer_logout_service_binding + end - # Setter for Single Logout Service Binding. - # - # (Currently we only support "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect") - # @param value [String, Symbol] - # - def single_logout_service_binding=(value) - @single_logout_service_binding = get_binding(value) - end + # Setter for Single Logout Service Binding. + # + # (Currently we only support "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect") + # @param value [String, Symbol] + # + def single_logout_service_binding=(value) + @single_logout_service_binding = get_binding(value) + end - # @deprecated Setter for legacy Single Logout Service Binding parameter. - # - # (Currently we only support "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect") - # @param value [String, Symbol] - # - def assertion_consumer_logout_service_binding=(value) - @assertion_consumer_logout_service_binding = get_binding(value) - end + # @deprecated Setter for legacy Single Logout Service Binding parameter. + # + # (Currently we only support "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect") + # @param value [String, Symbol] + # + def assertion_consumer_logout_service_binding=(value) + @assertion_consumer_logout_service_binding = get_binding(value) + end - # Calculates the fingerprint of the IdP x509 certificate. - # @return [String] The fingerprint - # - def get_fingerprint - idp_cert_fingerprint || begin - idp_cert = get_idp_cert - if idp_cert - fingerprint_alg = XMLSecurity::BaseDocument.new.algorithm(idp_cert_fingerprint_algorithm).new - fingerprint_alg.hexdigest(idp_cert.to_der).upcase.scan(/../).join(":") - end + # Calculates the fingerprint of the IdP x509 certificate. + # @return [String] The fingerprint + # + def get_fingerprint + idp_cert_fingerprint || begin + idp_cert = get_idp_cert + if idp_cert + fingerprint_alg = XMLSecurity::BaseDocument.new.algorithm(idp_cert_fingerprint_algorithm).new + fingerprint_alg.hexdigest(idp_cert.to_der).upcase.scan(/../).join(":") end end + end - # @return [OpenSSL::X509::Certificate|nil] Build the IdP certificate from the settings (previously format it) - # - def get_idp_cert - OneLogin::RubySaml::Utils.build_cert_object(idp_cert) - end + # @return [OpenSSL::X509::Certificate|nil] Build the IdP certificate from the settings (previously format it) + # + def get_idp_cert + RubySaml::Utils.build_cert_object(idp_cert) + end - # @return [Hash with 2 arrays of OpenSSL::X509::Certificate] Build multiple IdP certificates from the settings. - # - def get_idp_cert_multi - return nil if idp_cert_multi.nil? || idp_cert_multi.empty? + # @return [Hash with 2 arrays of OpenSSL::X509::Certificate] Build multiple IdP certificates from the settings. + # + def get_idp_cert_multi + return nil if idp_cert_multi.nil? || idp_cert_multi.empty? - raise ArgumentError.new("Invalid value for idp_cert_multi") unless idp_cert_multi.is_a?(Hash) + raise ArgumentError.new("Invalid value for idp_cert_multi") unless idp_cert_multi.is_a?(Hash) - certs = {signing: [], encryption: [] } + certs = {signing: [], encryption: [] } - %i[signing encryption].each do |type| - certs_for_type = idp_cert_multi[type] || idp_cert_multi[type.to_s] - next if !certs_for_type || certs_for_type.empty? + %i[signing encryption].each do |type| + certs_for_type = idp_cert_multi[type] || idp_cert_multi[type.to_s] + next if !certs_for_type || certs_for_type.empty? - certs_for_type.each do |idp_cert| - certs[type].push(OneLogin::RubySaml::Utils.build_cert_object(idp_cert)) - end + certs_for_type.each do |idp_cert| + certs[type].push(RubySaml::Utils.build_cert_object(idp_cert)) end - - certs end - # @return [Hash>>] - # Build the SP certificates and private keys from the settings. If - # check_sp_cert_expiration is true, only returns certificates and private keys - # that are not expired. - def get_sp_certs - certs = get_all_sp_certs - return certs unless security[:check_sp_cert_expiration] + certs + end - active_certs = { signing: [], encryption: [] } - certs.each do |use, pairs| - next if pairs.empty? + # @return [Hash>>] + # Build the SP certificates and private keys from the settings. If + # check_sp_cert_expiration is true, only returns certificates and private keys + # that are not expired. + def get_sp_certs + certs = get_all_sp_certs + return certs unless security[:check_sp_cert_expiration] - pairs = pairs.select { |cert, _| !cert || OneLogin::RubySaml::Utils.is_cert_active(cert) } - raise OneLogin::RubySaml::ValidationError.new("The SP certificate expired.") if pairs.empty? + active_certs = { signing: [], encryption: [] } + certs.each do |use, pairs| + next if pairs.empty? - active_certs[use] = pairs.freeze - end - active_certs.freeze - end + pairs = pairs.select { |cert, _| !cert || RubySaml::Utils.is_cert_active(cert) } + raise RubySaml::ValidationError.new("The SP certificate expired.") if pairs.empty? - # @return [Array] - # The SP signing certificate and private key. - def get_sp_signing_pair - get_sp_certs[:signing].first + active_certs[use] = pairs.freeze end + active_certs.freeze + end - # @return [OpenSSL::X509::Certificate] The SP signing certificate. - # @deprecated Use get_sp_signing_pair or get_sp_certs instead. - def get_sp_cert - node = get_sp_signing_pair - node[0] if node - end + # @return [Array] + # The SP signing certificate and private key. + def get_sp_signing_pair + get_sp_certs[:signing].first + end - # @return [OpenSSL::PKey::RSA] The SP signing key. - def get_sp_signing_key - node = get_sp_signing_pair - node[1] if node - end + # @return [OpenSSL::X509::Certificate] The SP signing certificate. + # @deprecated Use get_sp_signing_pair or get_sp_certs instead. + def get_sp_cert + node = get_sp_signing_pair + node[0] if node + end - # @deprecated Use get_sp_signing_key or get_sp_certs instead. - alias_method :get_sp_key, :get_sp_signing_key + # @return [OpenSSL::PKey::RSA] The SP signing key. + def get_sp_signing_key + node = get_sp_signing_pair + node[1] if node + end - # @return [Array] The SP decryption keys. - def get_sp_decryption_keys - ary = get_sp_certs[:encryption].map { |pair| pair[1] } - ary.compact! - ary.uniq!(&:to_pem) - ary.freeze - end + # @deprecated Use get_sp_signing_key or get_sp_certs instead. + alias_method :get_sp_key, :get_sp_signing_key - # @return [OpenSSL::X509::Certificate|nil] Build the New SP certificate from the settings. - # - # @deprecated Use get_sp_certs instead - def get_sp_cert_new - node = get_sp_certs[:signing].last - node[0] if node - end + # @return [Array] The SP decryption keys. + def get_sp_decryption_keys + ary = get_sp_certs[:encryption].map { |pair| pair[1] } + ary.compact! + ary.uniq!(&:to_pem) + ary.freeze + end - def idp_binding_from_embed_sign - security[:embed_sign] ? Utils::BINDINGS[:post] : Utils::BINDINGS[:redirect] - end + # @return [OpenSSL::X509::Certificate|nil] Build the New SP certificate from the settings. + # + # @deprecated Use get_sp_certs instead + def get_sp_cert_new + node = get_sp_certs[:signing].last + node[0] if node + end - def get_binding(value) - return unless value + def idp_binding_from_embed_sign + security[:embed_sign] ? Utils::BINDINGS[:post] : Utils::BINDINGS[:redirect] + end - Utils::BINDINGS[value.to_sym] || value - end + def get_binding(value) + return unless value - DEFAULTS = { - assertion_consumer_service_binding: Utils::BINDINGS[:post], - single_logout_service_binding: Utils::BINDINGS[:redirect], - idp_cert_fingerprint_algorithm: XMLSecurity::Document::SHA1, - compress_request: true, - compress_response: true, - message_max_bytesize: 250_000, - soft: true, - double_quote_xml_attribute_values: false, - security: { - authn_requests_signed: false, - logout_requests_signed: false, - logout_responses_signed: false, - want_assertions_signed: false, - want_assertions_encrypted: false, - want_name_id: false, - metadata_signed: false, - embed_sign: false, # Deprecated - digest_method: XMLSecurity::Document::SHA1, - signature_method: XMLSecurity::Document::RSA_SHA1, - check_idp_cert_expiration: false, - check_sp_cert_expiration: false, - strict_audience_validation: false, - lowercase_url_encoding: false - }.freeze + Utils::BINDINGS[value.to_sym] || value + end + + DEFAULTS = { + assertion_consumer_service_binding: Utils::BINDINGS[:post], + single_logout_service_binding: Utils::BINDINGS[:redirect], + idp_cert_fingerprint_algorithm: XMLSecurity::Document::SHA1, + compress_request: true, + compress_response: true, + message_max_bytesize: 250_000, + soft: true, + double_quote_xml_attribute_values: false, + security: { + authn_requests_signed: false, + logout_requests_signed: false, + logout_responses_signed: false, + want_assertions_signed: false, + want_assertions_encrypted: false, + want_name_id: false, + metadata_signed: false, + embed_sign: false, # Deprecated + digest_method: XMLSecurity::Document::SHA1, + signature_method: XMLSecurity::Document::RSA_SHA1, + check_idp_cert_expiration: false, + check_sp_cert_expiration: false, + strict_audience_validation: false, + lowercase_url_encoding: false }.freeze + }.freeze - private + private - # @return [Hash>>] - # Build the SP certificates and private keys from the settings. Returns all - # certificates and private keys, even if they are expired. - def get_all_sp_certs - validate_sp_certs_params! - get_sp_certs_multi || get_sp_certs_single - end + # @return [Hash>>] + # Build the SP certificates and private keys from the settings. Returns all + # certificates and private keys, even if they are expired. + def get_all_sp_certs + validate_sp_certs_params! + get_sp_certs_multi || get_sp_certs_single + end - # Validate certificate, certificate_new, private_key, and sp_cert_multi params. - def validate_sp_certs_params! - multi = sp_cert_multi && !sp_cert_multi.empty? - cert = certificate && !certificate.empty? - cert_new = certificate_new && !certificate_new.empty? - pk = private_key && !private_key.empty? - if multi && (cert || cert_new || pk) - raise ArgumentError.new("Cannot specify both sp_cert_multi and certificate, certificate_new, private_key parameters") - end + # Validate certificate, certificate_new, private_key, and sp_cert_multi params. + def validate_sp_certs_params! + multi = sp_cert_multi && !sp_cert_multi.empty? + cert = certificate && !certificate.empty? + cert_new = certificate_new && !certificate_new.empty? + pk = private_key && !private_key.empty? + if multi && (cert || cert_new || pk) + raise ArgumentError.new("Cannot specify both sp_cert_multi and certificate, certificate_new, private_key parameters") end + end - # Get certs from certificate, certificate_new, and private_key parameters. - def get_sp_certs_single - certs = { :signing => [], :encryption => [] } - - sp_key = OneLogin::RubySaml::Utils.build_private_key_object(private_key) - cert = OneLogin::RubySaml::Utils.build_cert_object(certificate) - if cert || sp_key - ary = [cert, sp_key].freeze - certs[:signing] << ary - certs[:encryption] << ary - end + # Get certs from certificate, certificate_new, and private_key parameters. + def get_sp_certs_single + certs = { :signing => [], :encryption => [] } - cert_new = OneLogin::RubySaml::Utils.build_cert_object(certificate_new) - if cert_new - ary = [cert_new, sp_key].freeze - certs[:signing] << ary - certs[:encryption] << ary - end + sp_key = RubySaml::Utils.build_private_key_object(private_key) + cert = RubySaml::Utils.build_cert_object(certificate) + if cert || sp_key + ary = [cert, sp_key].freeze + certs[:signing] << ary + certs[:encryption] << ary + end - certs + cert_new = RubySaml::Utils.build_cert_object(certificate_new) + if cert_new + ary = [cert_new, sp_key].freeze + certs[:signing] << ary + certs[:encryption] << ary end - # Get certs from get_sp_cert_multi parameter. - def get_sp_certs_multi - return if sp_cert_multi.nil? || sp_cert_multi.empty? + certs + end - raise ArgumentError.new("sp_cert_multi must be a Hash") unless sp_cert_multi.is_a?(Hash) + # Get certs from get_sp_cert_multi parameter. + def get_sp_certs_multi + return if sp_cert_multi.nil? || sp_cert_multi.empty? - certs = { :signing => [], :encryption => [] }.freeze + raise ArgumentError.new("sp_cert_multi must be a Hash") unless sp_cert_multi.is_a?(Hash) - [:signing, :encryption].each do |type| - certs_for_type = sp_cert_multi[type] || sp_cert_multi[type.to_s] - next if !certs_for_type || certs_for_type.empty? + certs = { :signing => [], :encryption => [] }.freeze - unless certs_for_type.is_a?(Array) && certs_for_type.all? { |cert| cert.is_a?(Hash) } - raise ArgumentError.new("sp_cert_multi :#{type} node must be an Array of Hashes") - end + [:signing, :encryption].each do |type| + certs_for_type = sp_cert_multi[type] || sp_cert_multi[type.to_s] + next if !certs_for_type || certs_for_type.empty? - certs_for_type.each do |pair| - cert = pair[:certificate] || pair['certificate'] || pair[:cert] || pair['cert'] - key = pair[:private_key] || pair['private_key'] || pair[:key] || pair['key'] + unless certs_for_type.is_a?(Array) && certs_for_type.all? { |cert| cert.is_a?(Hash) } + raise ArgumentError.new("sp_cert_multi :#{type} node must be an Array of Hashes") + end - unless cert && key - raise ArgumentError.new("sp_cert_multi :#{type} node Hashes must specify keys :certificate and :private_key") - end + certs_for_type.each do |pair| + cert = pair[:certificate] || pair['certificate'] || pair[:cert] || pair['cert'] + key = pair[:private_key] || pair['private_key'] || pair[:key] || pair['key'] - certs[type] << [ - OneLogin::RubySaml::Utils.build_cert_object(cert), - OneLogin::RubySaml::Utils.build_private_key_object(key) - ].freeze + unless cert && key + raise ArgumentError.new("sp_cert_multi :#{type} node Hashes must specify keys :certificate and :private_key") end - end - certs.each { |_, ary| ary.freeze } - certs + certs[type] << [ + RubySaml::Utils.build_cert_object(cert), + RubySaml::Utils.build_private_key_object(key) + ].freeze + end end + + certs.each { |_, ary| ary.freeze } + certs end end end - diff --git a/lib/onelogin/ruby-saml/slo_logoutrequest.rb b/lib/onelogin/ruby-saml/slo_logoutrequest.rb index 2199064c0..b3cd765bb 100644 --- a/lib/onelogin/ruby-saml/slo_logoutrequest.rb +++ b/lib/onelogin/ruby-saml/slo_logoutrequest.rb @@ -7,332 +7,330 @@ require "onelogin/ruby-saml/saml_message" # Only supports SAML 2.0 -module OneLogin - module RubySaml +module RubySaml - # SAML2 Logout Request (SLO IdP initiated, Parser) - # - class SloLogoutrequest < SamlMessage - include ErrorHandling - - # OneLogin::RubySaml::Settings Toolkit settings - attr_accessor :settings - - attr_reader :document - attr_reader :request - attr_reader :options - - attr_accessor :soft - - # Constructs the Logout Request. A Logout Request Object that is an extension of the SamlMessage class. - # @param request [String] A UUEncoded Logout Request from the IdP. - # @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object - # Or :allowed_clock_drift for the logout request validation process to allow a clock drift when checking dates with - # Or :relax_signature_validation to accept signatures if no idp certificate registered on settings - # - # @raise [ArgumentError] If Request is nil - # - def initialize(request, options = {}) - raise ArgumentError.new("Request cannot be nil") if request.nil? - - @errors = [] - @options = options - @soft = true - unless options[:settings].nil? - @settings = options[:settings] - @soft = @settings.soft unless @settings.soft.nil? - end + # SAML2 Logout Request (SLO IdP initiated, Parser) + # + class SloLogoutrequest < SamlMessage + include ErrorHandling - @request = decode_raw_saml(request, settings) - @document = REXML::Document.new(@request) - super() - end + # RubySaml::Settings Toolkit settings + attr_accessor :settings - def request_id - id(document) - end + attr_reader :document + attr_reader :request + attr_reader :options - # Validates the Logout Request with the default values (soft = true) - # @param collect_errors [Boolean] Stop validation when first error appears or keep validating. - # @return [Boolean] TRUE if the Logout Request is valid - # - def is_valid?(collect_errors = false) - validate(collect_errors) - end + attr_accessor :soft - # @return [String] Gets the NameID of the Logout Request. - # - def name_id - @name_id ||= Utils.element_text(name_id_node) + # Constructs the Logout Request. A Logout Request Object that is an extension of the SamlMessage class. + # @param request [String] A UUEncoded Logout Request from the IdP. + # @param options [Hash] :settings to provide the RubySaml::Settings object + # Or :allowed_clock_drift for the logout request validation process to allow a clock drift when checking dates with + # Or :relax_signature_validation to accept signatures if no idp certificate registered on settings + # + # @raise [ArgumentError] If Request is nil + # + def initialize(request, options = {}) + raise ArgumentError.new("Request cannot be nil") if request.nil? + + @errors = [] + @options = options + @soft = true + unless options[:settings].nil? + @settings = options[:settings] + @soft = @settings.soft unless @settings.soft.nil? end - alias_method :nameid, :name_id + @request = decode_raw_saml(request, settings) + @document = REXML::Document.new(@request) + super() + end - # @return [String] Gets the NameID Format of the Logout Request. - # - def name_id_format - @name_id_format ||= - if name_id_node && name_id_node.attribute("Format") - name_id_node.attribute("Format").value - end - end + def request_id + id(document) + end - alias_method :nameid_format, :name_id_format - - def name_id_node - @name_id_node ||= - begin - encrypted_node = REXML::XPath.first(document, "/p:LogoutRequest/a:EncryptedID", { "p" => PROTOCOL, "a" => ASSERTION }) - if encrypted_node - node = decrypt_nameid(encrypted_node) - else - node = REXML::XPath.first(document, "/p:LogoutRequest/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION }) - end - end - end + # Validates the Logout Request with the default values (soft = true) + # @param collect_errors [Boolean] Stop validation when first error appears or keep validating. + # @return [Boolean] TRUE if the Logout Request is valid + # + def is_valid?(collect_errors = false) + validate(collect_errors) + end - # Decrypts an EncryptedID element - # @param encrypted_id_node [REXML::Element] The EncryptedID element - # @return [REXML::Document] The decrypted EncrypedtID element - # - def decrypt_nameid(encrypted_id_node) + # @return [String] Gets the NameID of the Logout Request. + # + def name_id + @name_id ||= Utils.element_text(name_id_node) + end - if settings.nil? || settings.get_sp_decryption_keys.empty? - raise ValidationError.new('An ' + encrypted_id_node.name + ' found and no SP private key found on the settings to decrypt it') - end + alias_method :nameid, :name_id - elem_plaintext = OneLogin::RubySaml::Utils.decrypt_multi(encrypted_id_node, settings.get_sp_decryption_keys) - # If we get some problematic noise in the plaintext after decrypting. - # This quick regexp parse will grab only the Element and discard the noise. - elem_plaintext = elem_plaintext.match(/(.*<\/(\w+:)?NameID>)/m)[0] - - # To avoid namespace errors if saml namespace is not defined - # create a parent node first with the namespace defined - node_header = '' - elem_plaintext = node_header + elem_plaintext + '' - doc = REXML::Document.new(elem_plaintext) - doc.root[0] - end + # @return [String] Gets the NameID Format of the Logout Request. + # + def name_id_format + @name_id_format ||= + if name_id_node && name_id_node.attribute("Format") + name_id_node.attribute("Format").value + end + end - # @return [String|nil] Gets the ID attribute from the Logout Request. if exists. - # - def id - super(document) - end + alias_method :nameid_format, :name_id_format - # @return [String] Gets the Issuer from the Logout Request. - # - def issuer - @issuer ||= begin - node = REXML::XPath.first( - document, - "/p:LogoutRequest/a:Issuer", - { "p" => PROTOCOL, "a" => ASSERTION } - ) - Utils.element_text(node) + def name_id_node + @name_id_node ||= + begin + encrypted_node = REXML::XPath.first(document, "/p:LogoutRequest/a:EncryptedID", { "p" => PROTOCOL, "a" => ASSERTION }) + if encrypted_node + node = decrypt_nameid(encrypted_node) + else + node = REXML::XPath.first(document, "/p:LogoutRequest/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION }) + end end + end + + # Decrypts an EncryptedID element + # @param encrypted_id_node [REXML::Element] The EncryptedID element + # @return [REXML::Document] The decrypted EncrypedtID element + # + def decrypt_nameid(encrypted_id_node) + + if settings.nil? || settings.get_sp_decryption_keys.empty? + raise ValidationError.new('An ' + encrypted_id_node.name + ' found and no SP private key found on the settings to decrypt it') end - # @return [Time|nil] Gets the NotOnOrAfter Attribute value if exists. - # - def not_on_or_after - @not_on_or_after ||= begin - node = REXML::XPath.first( - document, - "/p:LogoutRequest", - { "p" => PROTOCOL } - ) - if node && node.attributes["NotOnOrAfter"] - Time.parse(node.attributes["NotOnOrAfter"]) - end - end + elem_plaintext = RubySaml::Utils.decrypt_multi(encrypted_id_node, settings.get_sp_decryption_keys) + # If we get some problematic noise in the plaintext after decrypting. + # This quick regexp parse will grab only the Element and discard the noise. + elem_plaintext = elem_plaintext.match(/(.*<\/(\w+:)?NameID>)/m)[0] + + # To avoid namespace errors if saml namespace is not defined + # create a parent node first with the namespace defined + node_header = '' + elem_plaintext = node_header + elem_plaintext + '' + doc = REXML::Document.new(elem_plaintext) + doc.root[0] + end + + # @return [String|nil] Gets the ID attribute from the Logout Request. if exists. + # + def id + super(document) + end + + # @return [String] Gets the Issuer from the Logout Request. + # + def issuer + @issuer ||= begin + node = REXML::XPath.first( + document, + "/p:LogoutRequest/a:Issuer", + { "p" => PROTOCOL, "a" => ASSERTION } + ) + Utils.element_text(node) end + end - # @return [Array] Gets the SessionIndex if exists (Supported multiple values). Empty Array if none found - # - def session_indexes - nodes = REXML::XPath.match( + # @return [Time|nil] Gets the NotOnOrAfter Attribute value if exists. + # + def not_on_or_after + @not_on_or_after ||= begin + node = REXML::XPath.first( document, - "/p:LogoutRequest/p:SessionIndex", + "/p:LogoutRequest", { "p" => PROTOCOL } ) - - nodes.map { |node| Utils.element_text(node) } + if node && node.attributes["NotOnOrAfter"] + Time.parse(node.attributes["NotOnOrAfter"]) + end end + end - private + # @return [Array] Gets the SessionIndex if exists (Supported multiple values). Empty Array if none found + # + def session_indexes + nodes = REXML::XPath.match( + document, + "/p:LogoutRequest/p:SessionIndex", + { "p" => PROTOCOL } + ) + + nodes.map { |node| Utils.element_text(node) } + end - # returns the allowed clock drift on timing validation - # @return [Float] - def allowed_clock_drift - options[:allowed_clock_drift].to_f.abs + Float::EPSILON - end + private - # Hard aux function to validate the Logout Request - # @param collect_errors [Boolean] Stop validation when first error appears or keep validating. (if soft=true) - # @return [Boolean] TRUE if the Logout Request is valid - # @raise [ValidationError] if soft == false and validation fails - # - def validate(collect_errors = false) - reset_errors! - - validations = %i[ - validate_request_state - validate_id - validate_version - validate_structure - validate_not_on_or_after - validate_issuer - validate_signature - ] - - if collect_errors - validations.each { |validation| send(validation) } - @errors.empty? - else - validations.all? { |validation| send(validation) } - end - end + # returns the allowed clock drift on timing validation + # @return [Float] + def allowed_clock_drift + options[:allowed_clock_drift].to_f.abs + Float::EPSILON + end - # Validates that the Logout Request contains an ID - # If fails, the error is added to the errors array. - # @return [Boolean] True if the Logout Request contains an ID, otherwise returns False - # - def validate_id - return true if id - append_error("Missing ID attribute on Logout Request") + # Hard aux function to validate the Logout Request + # @param collect_errors [Boolean] Stop validation when first error appears or keep validating. (if soft=true) + # @return [Boolean] TRUE if the Logout Request is valid + # @raise [ValidationError] if soft == false and validation fails + # + def validate(collect_errors = false) + reset_errors! + + validations = %i[ + validate_request_state + validate_id + validate_version + validate_structure + validate_not_on_or_after + validate_issuer + validate_signature + ] + + if collect_errors + validations.each { |validation| send(validation) } + @errors.empty? + else + validations.all? { |validation| send(validation) } end + end - # Validates the SAML version (2.0) - # If fails, the error is added to the errors array. - # @return [Boolean] True if the Logout Request is 2.0, otherwise returns False - # - def validate_version - return true if version(document) == "2.0" - append_error("Unsupported SAML version") - end + # Validates that the Logout Request contains an ID + # If fails, the error is added to the errors array. + # @return [Boolean] True if the Logout Request contains an ID, otherwise returns False + # + def validate_id + return true if id + append_error("Missing ID attribute on Logout Request") + end - # Validates the time. (If the logout request was initialized with the :allowed_clock_drift - # option, the timing validations are relaxed by the allowed_clock_drift value) - # If fails, the error is added to the errors array - # @return [Boolean] True if satisfies the conditions, otherwise False if soft=True - # @raise [ValidationError] if soft == false and validation fails - # - def validate_not_on_or_after - now = Time.now.utc - - if not_on_or_after && now >= (not_on_or_after + allowed_clock_drift) - return append_error("Current time is on or after NotOnOrAfter (#{now} >= #{not_on_or_after}#{" + #{allowed_clock_drift.ceil}s" if allowed_clock_drift > 0})") - end + # Validates the SAML version (2.0) + # If fails, the error is added to the errors array. + # @return [Boolean] True if the Logout Request is 2.0, otherwise returns False + # + def validate_version + return true if version(document) == "2.0" + append_error("Unsupported SAML version") + end + + # Validates the time. (If the logout request was initialized with the :allowed_clock_drift + # option, the timing validations are relaxed by the allowed_clock_drift value) + # If fails, the error is added to the errors array + # @return [Boolean] True if satisfies the conditions, otherwise False if soft=True + # @raise [ValidationError] if soft == false and validation fails + # + def validate_not_on_or_after + now = Time.now.utc - true + if not_on_or_after && now >= (not_on_or_after + allowed_clock_drift) + return append_error("Current time is on or after NotOnOrAfter (#{now} >= #{not_on_or_after}#{" + #{allowed_clock_drift.ceil}s" if allowed_clock_drift > 0})") end - # Validates the Logout Request against the specified schema. - # @return [Boolean] True if the XML is valid, otherwise False if soft=True - # @raise [ValidationError] if soft == false and validation fails - # - def validate_structure - unless valid_saml?(document, soft) - return append_error("Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd") - end + true + end - true + # Validates the Logout Request against the specified schema. + # @return [Boolean] True if the XML is valid, otherwise False if soft=True + # @raise [ValidationError] if soft == false and validation fails + # + def validate_structure + unless valid_saml?(document, soft) + return append_error("Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd") end - # Validates that the Logout Request provided in the initialization is not empty, - # @return [Boolean] True if the required info is found, otherwise False if soft=True - # @raise [ValidationError] if soft == false and validation fails - # - def validate_request_state - return append_error("Blank logout request") if request.nil? || request.empty? + true + end - true - end + # Validates that the Logout Request provided in the initialization is not empty, + # @return [Boolean] True if the required info is found, otherwise False if soft=True + # @raise [ValidationError] if soft == false and validation fails + # + def validate_request_state + return append_error("Blank logout request") if request.nil? || request.empty? - # Validates the Issuer of the Logout Request - # If fails, the error is added to the errors array - # @return [Boolean] True if the Issuer matchs the IdP entityId, otherwise False if soft=True - # @raise [ValidationError] if soft == false and validation fails - # - def validate_issuer - return true if settings.nil? || settings.idp_entity_id.nil? || issuer.nil? + true + end - unless OneLogin::RubySaml::Utils.uri_match?(issuer, settings.idp_entity_id) - return append_error("Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>") - end + # Validates the Issuer of the Logout Request + # If fails, the error is added to the errors array + # @return [Boolean] True if the Issuer matchs the IdP entityId, otherwise False if soft=True + # @raise [ValidationError] if soft == false and validation fails + # + def validate_issuer + return true if settings.nil? || settings.idp_entity_id.nil? || issuer.nil? - true + unless RubySaml::Utils.uri_match?(issuer, settings.idp_entity_id) + return append_error("Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>") end - # Validates the Signature if exists and GET parameters are provided - # @return [Boolean] True if not contains a Signature or if the Signature is valid, otherwise False if soft=True - # @raise [ValidationError] if soft == false and validation fails - # - def validate_signature - return true if options.nil? - return true unless options.key? :get_params - return true unless options[:get_params].key? 'Signature' + true + end - options[:raw_get_params] = OneLogin::RubySaml::Utils.prepare_raw_get_params(options[:raw_get_params], options[:get_params], settings.security[:lowercase_url_encoding]) + # Validates the Signature if exists and GET parameters are provided + # @return [Boolean] True if not contains a Signature or if the Signature is valid, otherwise False if soft=True + # @raise [ValidationError] if soft == false and validation fails + # + def validate_signature + return true if options.nil? + return true unless options.key? :get_params + return true unless options[:get_params].key? 'Signature' - if options[:get_params]['SigAlg'].nil? && !options[:raw_get_params]['SigAlg'].nil? - options[:get_params]['SigAlg'] = CGI.unescape(options[:raw_get_params]['SigAlg']) - end + options[:raw_get_params] = RubySaml::Utils.prepare_raw_get_params(options[:raw_get_params], options[:get_params], settings.security[:lowercase_url_encoding]) - idp_cert = settings.get_idp_cert - idp_certs = settings.get_idp_cert_multi + if options[:get_params]['SigAlg'].nil? && !options[:raw_get_params]['SigAlg'].nil? + options[:get_params]['SigAlg'] = CGI.unescape(options[:raw_get_params]['SigAlg']) + end - if idp_cert.nil? && (idp_certs.nil? || idp_certs[:signing].empty?) - return options.key? :relax_signature_validation - end + idp_cert = settings.get_idp_cert + idp_certs = settings.get_idp_cert_multi - query_string = OneLogin::RubySaml::Utils.build_query_from_raw_parts( - type: 'SAMLRequest', - raw_data: options[:raw_get_params]['SAMLRequest'], - raw_relay_state: options[:raw_get_params]['RelayState'], - raw_sig_alg: options[:raw_get_params]['SigAlg'] - ) + if idp_cert.nil? && (idp_certs.nil? || idp_certs[:signing].empty?) + return options.key? :relax_signature_validation + end - expired = false - if idp_certs.nil? || idp_certs[:signing].empty? - valid = OneLogin::RubySaml::Utils.verify_signature( - cert: idp_cert, + query_string = RubySaml::Utils.build_query_from_raw_parts( + type: 'SAMLRequest', + raw_data: options[:raw_get_params]['SAMLRequest'], + raw_relay_state: options[:raw_get_params]['RelayState'], + raw_sig_alg: options[:raw_get_params]['SigAlg'] + ) + + expired = false + if idp_certs.nil? || idp_certs[:signing].empty? + valid = RubySaml::Utils.verify_signature( + cert: idp_cert, + sig_alg: options[:get_params]['SigAlg'], + signature: options[:get_params]['Signature'], + query_string: query_string + ) + if valid && settings.security[:check_idp_cert_expiration] && RubySaml::Utils.is_cert_expired(idp_cert) + expired = true + end + else + valid = false + idp_certs[:signing].each do |signing_idp_cert| + valid = RubySaml::Utils.verify_signature( + cert: signing_idp_cert, sig_alg: options[:get_params]['SigAlg'], signature: options[:get_params]['Signature'], query_string: query_string ) - if valid && settings.security[:check_idp_cert_expiration] && OneLogin::RubySaml::Utils.is_cert_expired(idp_cert) + next unless valid + + if settings.security[:check_idp_cert_expiration] && RubySaml::Utils.is_cert_expired(signing_idp_cert) expired = true end - else - valid = false - idp_certs[:signing].each do |signing_idp_cert| - valid = OneLogin::RubySaml::Utils.verify_signature( - cert: signing_idp_cert, - sig_alg: options[:get_params]['SigAlg'], - signature: options[:get_params]['Signature'], - query_string: query_string - ) - next unless valid - - if settings.security[:check_idp_cert_expiration] && OneLogin::RubySaml::Utils.is_cert_expired(signing_idp_cert) - expired = true - end - break - end - end - - if expired - error_msg = "IdP x509 certificate expired" - return append_error(error_msg) - end - unless valid - return append_error("Invalid Signature on Logout Request") + break end + end - true + if expired + error_msg = "IdP x509 certificate expired" + return append_error(error_msg) end + unless valid + return append_error("Invalid Signature on Logout Request") + end + + true end end end diff --git a/lib/onelogin/ruby-saml/slo_logoutresponse.rb b/lib/onelogin/ruby-saml/slo_logoutresponse.rb index d8386c428..d481a5c7c 100644 --- a/lib/onelogin/ruby-saml/slo_logoutresponse.rb +++ b/lib/onelogin/ruby-saml/slo_logoutresponse.rb @@ -7,159 +7,157 @@ require "onelogin/ruby-saml/setting_error" # Only supports SAML 2.0 -module OneLogin - module RubySaml +module RubySaml - # SAML2 Logout Response (SLO SP initiated, Parser) + # SAML2 Logout Response (SLO SP initiated, Parser) + # + class SloLogoutresponse < SamlMessage + + # Logout Response ID + attr_accessor :uuid + + # Initializes the Logout Response. A SloLogoutresponse Object that is an extension of the SamlMessage class. + # Asigns an ID, a random uuid. # - class SloLogoutresponse < SamlMessage + def initialize + @uuid = RubySaml::Utils.uuid + super() + end - # Logout Response ID - attr_accessor :uuid + def response_id + @uuid + end - # Initializes the Logout Response. A SloLogoutresponse Object that is an extension of the SamlMessage class. - # Asigns an ID, a random uuid. - # - def initialize - @uuid = OneLogin::RubySaml::Utils.uuid - super() + # Creates the Logout Response string. + # @param settings [RubySaml::Settings|nil] Toolkit settings + # @param request_id [String] The ID of the LogoutRequest sent by this SP to the IdP. That ID will be placed as the InResponseTo in the logout response + # @param logout_message [String] The Message to be placed as StatusMessage in the logout response + # @param params [Hash] Some extra parameters to be added in the GET for example the RelayState + # @param logout_status_code [String] The StatusCode to be placed as StatusMessage in the logout response + # @return [String] Logout Request string that includes the SAMLRequest + # + def create(settings, request_id = nil, logout_message = nil, params = {}, logout_status_code = nil) + params = create_params(settings, request_id, logout_message, params, logout_status_code) + params_prefix = /\?/.match?(settings.idp_slo_service_url) ? '&' : '?' + url = settings.idp_slo_response_service_url || settings.idp_slo_service_url + saml_response = CGI.escape(params.delete("SAMLResponse")) + response_params = +"#{params_prefix}SAMLResponse=#{saml_response}" + params.each_pair do |key, value| + response_params << "&#{key}=#{CGI.escape(value.to_s)}" end - def response_id - @uuid - end + raise SettingError.new "Invalid settings, idp_slo_service_url is not set!" if url.nil? or url.empty? + @logout_url = url + response_params + end - # Creates the Logout Response string. - # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings - # @param request_id [String] The ID of the LogoutRequest sent by this SP to the IdP. That ID will be placed as the InResponseTo in the logout response - # @param logout_message [String] The Message to be placed as StatusMessage in the logout response - # @param params [Hash] Some extra parameters to be added in the GET for example the RelayState - # @param logout_status_code [String] The StatusCode to be placed as StatusMessage in the logout response - # @return [String] Logout Request string that includes the SAMLRequest - # - def create(settings, request_id = nil, logout_message = nil, params = {}, logout_status_code = nil) - params = create_params(settings, request_id, logout_message, params, logout_status_code) - params_prefix = /\?/.match?(settings.idp_slo_service_url) ? '&' : '?' - url = settings.idp_slo_response_service_url || settings.idp_slo_service_url - saml_response = CGI.escape(params.delete("SAMLResponse")) - response_params = +"#{params_prefix}SAMLResponse=#{saml_response}" - params.each_pair do |key, value| - response_params << "&#{key}=#{CGI.escape(value.to_s)}" - end - - raise SettingError.new "Invalid settings, idp_slo_service_url is not set!" if url.nil? or url.empty? - @logout_url = url + response_params + # Creates the Get parameters for the logout response. + # @param settings [RubySaml::Settings|nil] Toolkit settings + # @param request_id [String] The ID of the LogoutRequest sent by this SP to the IdP. That ID will be placed as the InResponseTo in the logout response + # @param logout_message [String] The Message to be placed as StatusMessage in the logout response + # @param params [Hash] Some extra parameters to be added in the GET for example the RelayState + # @param logout_status_code [String] The StatusCode to be placed as StatusMessage in the logout response + # @return [Hash] Parameters + # + def create_params(settings, request_id = nil, logout_message = nil, params = {}, logout_status_code = nil) + # The method expects :RelayState but sometimes we get 'RelayState' instead. + # Based on the HashWithIndifferentAccess value in Rails we could experience + # conflicts so this line will solve them. + relay_state = params[:RelayState] || params['RelayState'] + + if relay_state.nil? + params.delete(:RelayState) + params.delete('RelayState') end - # Creates the Get parameters for the logout response. - # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings - # @param request_id [String] The ID of the LogoutRequest sent by this SP to the IdP. That ID will be placed as the InResponseTo in the logout response - # @param logout_message [String] The Message to be placed as StatusMessage in the logout response - # @param params [Hash] Some extra parameters to be added in the GET for example the RelayState - # @param logout_status_code [String] The StatusCode to be placed as StatusMessage in the logout response - # @return [Hash] Parameters - # - def create_params(settings, request_id = nil, logout_message = nil, params = {}, logout_status_code = nil) - # The method expects :RelayState but sometimes we get 'RelayState' instead. - # Based on the HashWithIndifferentAccess value in Rails we could experience - # conflicts so this line will solve them. - relay_state = params[:RelayState] || params['RelayState'] - - if relay_state.nil? - params.delete(:RelayState) - params.delete('RelayState') - end - - response_doc = create_logout_response_xml_doc(settings, request_id, logout_message, logout_status_code) - response_doc.context[:attribute_quote] = :quote if settings.double_quote_xml_attribute_values - - response = +"" - response_doc.write(response) - - Logging.debug "Created SLO Logout Response: #{response}" - - response = deflate(response) if settings.compress_response - base64_response = encode(response) - response_params = {"SAMLResponse" => base64_response} - sp_signing_key = settings.get_sp_signing_key - - if settings.idp_slo_service_binding == Utils::BINDINGS[:redirect] && settings.security[:logout_responses_signed] && sp_signing_key - params['SigAlg'] = settings.security[:signature_method] - url_string = OneLogin::RubySaml::Utils.build_query( - type: 'SAMLResponse', - data: base64_response, - relay_state: relay_state, - sig_alg: params['SigAlg'] - ) - sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method]) - signature = sp_signing_key.sign(sign_algorithm.new, url_string) - params['Signature'] = encode(signature) - end - - params.each_pair do |key, value| - response_params[key] = value.to_s - end - - response_params + response_doc = create_logout_response_xml_doc(settings, request_id, logout_message, logout_status_code) + response_doc.context[:attribute_quote] = :quote if settings.double_quote_xml_attribute_values + + response = +"" + response_doc.write(response) + + Logging.debug "Created SLO Logout Response: #{response}" + + response = deflate(response) if settings.compress_response + base64_response = encode(response) + response_params = {"SAMLResponse" => base64_response} + sp_signing_key = settings.get_sp_signing_key + + if settings.idp_slo_service_binding == Utils::BINDINGS[:redirect] && settings.security[:logout_responses_signed] && sp_signing_key + params['SigAlg'] = settings.security[:signature_method] + url_string = RubySaml::Utils.build_query( + type: 'SAMLResponse', + data: base64_response, + relay_state: relay_state, + sig_alg: params['SigAlg'] + ) + sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method]) + signature = sp_signing_key.sign(sign_algorithm.new, url_string) + params['Signature'] = encode(signature) end - # Creates the SAMLResponse String. - # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings - # @param request_id [String] The ID of the LogoutRequest sent by this SP to the IdP. That ID will be placed as the InResponseTo in the logout response - # @param logout_message [String] The Message to be placed as StatusMessage in the logout response - # @param logout_status_code [String] The StatusCode to be placed as StatusMessage in the logout response - # @return [String] The SAMLResponse String. - # - def create_logout_response_xml_doc(settings, request_id = nil, logout_message = nil, logout_status_code = nil) - document = create_xml_document(settings, request_id, logout_message, logout_status_code) - sign_document(document, settings) + params.each_pair do |key, value| + response_params[key] = value.to_s end - def create_xml_document(settings, request_id = nil, logout_message = nil, status_code = nil) - time = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ') + response_params + end + + # Creates the SAMLResponse String. + # @param settings [RubySaml::Settings|nil] Toolkit settings + # @param request_id [String] The ID of the LogoutRequest sent by this SP to the IdP. That ID will be placed as the InResponseTo in the logout response + # @param logout_message [String] The Message to be placed as StatusMessage in the logout response + # @param logout_status_code [String] The StatusCode to be placed as StatusMessage in the logout response + # @return [String] The SAMLResponse String. + # + def create_logout_response_xml_doc(settings, request_id = nil, logout_message = nil, logout_status_code = nil) + document = create_xml_document(settings, request_id, logout_message, logout_status_code) + sign_document(document, settings) + end - response_doc = XMLSecurity::Document.new - response_doc.uuid = uuid + def create_xml_document(settings, request_id = nil, logout_message = nil, status_code = nil) + time = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ') - destination = settings.idp_slo_response_service_url || settings.idp_slo_service_url + response_doc = XMLSecurity::Document.new + response_doc.uuid = uuid - root = response_doc.add_element 'samlp:LogoutResponse', { 'xmlns:samlp' => 'urn:oasis:names:tc:SAML:2.0:protocol', "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion" } - root.attributes['ID'] = uuid - root.attributes['IssueInstant'] = time - root.attributes['Version'] = '2.0' - root.attributes['InResponseTo'] = request_id unless request_id.nil? - root.attributes['Destination'] = destination unless destination.nil? or destination.empty? + destination = settings.idp_slo_response_service_url || settings.idp_slo_service_url - unless settings.sp_entity_id.nil? - issuer = root.add_element "saml:Issuer" - issuer.text = settings.sp_entity_id - end + root = response_doc.add_element 'samlp:LogoutResponse', { 'xmlns:samlp' => 'urn:oasis:names:tc:SAML:2.0:protocol', "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion" } + root.attributes['ID'] = uuid + root.attributes['IssueInstant'] = time + root.attributes['Version'] = '2.0' + root.attributes['InResponseTo'] = request_id unless request_id.nil? + root.attributes['Destination'] = destination unless destination.nil? or destination.empty? - # add status - status = root.add_element 'samlp:Status' + unless settings.sp_entity_id.nil? + issuer = root.add_element "saml:Issuer" + issuer.text = settings.sp_entity_id + end - # status code - status_code ||= 'urn:oasis:names:tc:SAML:2.0:status:Success' - status_code_elem = status.add_element 'samlp:StatusCode' - status_code_elem.attributes['Value'] = status_code + # add status + status = root.add_element 'samlp:Status' - # status message - logout_message ||= 'Successfully Signed Out' - status_message = status.add_element 'samlp:StatusMessage' - status_message.text = logout_message + # status code + status_code ||= 'urn:oasis:names:tc:SAML:2.0:status:Success' + status_code_elem = status.add_element 'samlp:StatusCode' + status_code_elem.attributes['Value'] = status_code - response_doc - end + # status message + logout_message ||= 'Successfully Signed Out' + status_message = status.add_element 'samlp:StatusMessage' + status_message.text = logout_message - def sign_document(document, settings) - # embed signature - cert, private_key = settings.get_sp_signing_pair - if settings.idp_slo_service_binding == Utils::BINDINGS[:post] && private_key && cert - document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method]) - end + response_doc + end - document + def sign_document(document, settings) + # embed signature + cert, private_key = settings.get_sp_signing_pair + if settings.idp_slo_service_binding == Utils::BINDINGS[:post] && private_key && cert + document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method]) end + + document end end end diff --git a/lib/onelogin/ruby-saml/utils.rb b/lib/onelogin/ruby-saml/utils.rb index 3c91b29a5..29810b532 100644 --- a/lib/onelogin/ruby-saml/utils.rb +++ b/lib/onelogin/ruby-saml/utils.rb @@ -3,432 +3,430 @@ require 'securerandom' require "openssl" -module OneLogin - module RubySaml +module RubySaml + + # SAML2 Auxiliary class + # + class Utils + BINDINGS = { post: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", + redirect: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }.freeze + DSIG = "http://www.w3.org/2000/09/xmldsig#" + XENC = "http://www.w3.org/2001/04/xmlenc#" + DURATION_FORMAT = /^ + (-?)P # 1: Duration sign + (?: + (?:(\d+)Y)? # 2: Years + (?:(\d+)M)? # 3: Months + (?:(\d+)D)? # 4: Days + (?:T + (?:(\d+)H)? # 5: Hours + (?:(\d+)M)? # 6: Minutes + (?:(\d+(?:[.,]\d+)?)S)? # 7: Seconds + )? + | + (\d+)W # 8: Weeks + ) + $/x + UUID_PREFIX = +'_' + + # Checks if the x509 cert provided is expired. + # + # @param cert [OpenSSL::X509::Certificate|String] The x509 certificate. + # @return [true|false] Whether the certificate is expired. + def self.is_cert_expired(cert) + cert = OpenSSL::X509::Certificate.new(cert) if cert.is_a?(String) + + cert.not_after < Time.now + end - # SAML2 Auxiliary class + # Checks if the x509 cert provided has both started and has not expired. # - class Utils - BINDINGS = { post: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", - redirect: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }.freeze - DSIG = "http://www.w3.org/2000/09/xmldsig#" - XENC = "http://www.w3.org/2001/04/xmlenc#" - DURATION_FORMAT = /^ - (-?)P # 1: Duration sign - (?: - (?:(\d+)Y)? # 2: Years - (?:(\d+)M)? # 3: Months - (?:(\d+)D)? # 4: Days - (?:T - (?:(\d+)H)? # 5: Hours - (?:(\d+)M)? # 6: Minutes - (?:(\d+(?:[.,]\d+)?)S)? # 7: Seconds - )? - | - (\d+)W # 8: Weeks - ) - $/x - UUID_PREFIX = +'_' - - # Checks if the x509 cert provided is expired. - # - # @param cert [OpenSSL::X509::Certificate|String] The x509 certificate. - # @return [true|false] Whether the certificate is expired. - def self.is_cert_expired(cert) - cert = OpenSSL::X509::Certificate.new(cert) if cert.is_a?(String) - - cert.not_after < Time.now - end + # @param cert [OpenSSL::X509::Certificate|String] The x509 certificate. + # @return [true|false] Whether the certificate is currently active. + def self.is_cert_active(cert) + cert = OpenSSL::X509::Certificate.new(cert) if cert.is_a?(String) + now = Time.now + cert.not_before <= now && cert.not_after >= now + end - # Checks if the x509 cert provided has both started and has not expired. - # - # @param cert [OpenSSL::X509::Certificate|String] The x509 certificate. - # @return [true|false] Whether the certificate is currently active. - def self.is_cert_active(cert) - cert = OpenSSL::X509::Certificate.new(cert) if cert.is_a?(String) - now = Time.now - cert.not_before <= now && cert.not_after >= now + # Interprets a ISO8601 duration value relative to a given timestamp. + # + # @param duration [String] The duration, as a string. + # @param timestamp [Integer] The unix timestamp we should apply the + # duration to. Optional, default to the + # current time. + # + # @return [Integer] The new timestamp, after the duration is applied. + # + def self.parse_duration(duration, timestamp=Time.now.utc) + matches = duration.match(DURATION_FORMAT) + + if matches.nil? + raise StandardError.new("Invalid ISO 8601 duration") end - # Interprets a ISO8601 duration value relative to a given timestamp. - # - # @param duration [String] The duration, as a string. - # @param timestamp [Integer] The unix timestamp we should apply the - # duration to. Optional, default to the - # current time. - # - # @return [Integer] The new timestamp, after the duration is applied. - # - def self.parse_duration(duration, timestamp=Time.now.utc) - matches = duration.match(DURATION_FORMAT) - - if matches.nil? - raise StandardError.new("Invalid ISO 8601 duration") - end + sign = matches[1] == '-' ? -1 : 1 - sign = matches[1] == '-' ? -1 : 1 + durYears, durMonths, durDays, durHours, durMinutes, durSeconds, durWeeks = + matches[2..8].map { |match| match ? sign * match.tr(',', '.').to_f : 0.0 } - durYears, durMonths, durDays, durHours, durMinutes, durSeconds, durWeeks = - matches[2..8].map { |match| match ? sign * match.tr(',', '.').to_f : 0.0 } + initial_datetime = Time.at(timestamp).utc.to_datetime + final_datetime = initial_datetime.next_year(durYears) + final_datetime = final_datetime.next_month(durMonths) + final_datetime = final_datetime.next_day((7*durWeeks) + durDays) + final_datetime.to_time.utc.to_i + (durHours * 3600) + (durMinutes * 60) + durSeconds + end - initial_datetime = Time.at(timestamp).utc.to_datetime - final_datetime = initial_datetime.next_year(durYears) - final_datetime = final_datetime.next_month(durMonths) - final_datetime = final_datetime.next_day((7*durWeeks) + durDays) - final_datetime.to_time.utc.to_i + (durHours * 3600) + (durMinutes * 60) + durSeconds + # Return a properly formatted x509 certificate + # + # @param cert [String] The original certificate + # @return [String] The formatted certificate + # + def self.format_cert(cert) + # don't try to format an encoded certificate or if is empty or nil + if cert.respond_to?(:ascii_only?) + return cert if cert.nil? || cert.empty? || !cert.ascii_only? + elsif cert.nil? || cert.empty? || cert.match(/\x0d/) + return cert end - # Return a properly formatted x509 certificate - # - # @param cert [String] The original certificate - # @return [String] The formatted certificate - # - def self.format_cert(cert) - # don't try to format an encoded certificate or if is empty or nil - if cert.respond_to?(:ascii_only?) - return cert if cert.nil? || cert.empty? || !cert.ascii_only? - elsif cert.nil? || cert.empty? || cert.match(/\x0d/) - return cert - end - - if cert.scan(/BEGIN CERTIFICATE/).length > 1 - formatted_cert = [] - cert.scan(/-{5}BEGIN CERTIFICATE-{5}[\n\r]?.*?-{5}END CERTIFICATE-{5}[\n\r]?/m) do |c| - formatted_cert << format_cert(c) - end - formatted_cert.join("\n") - else - cert = cert.gsub(/-{5}\s?(BEGIN|END) CERTIFICATE\s?-{5}/, "") - cert = cert.gsub(/\r/, "") - cert = cert.gsub(/\n/, "") - cert = cert.gsub(/\s/, "") - cert = cert.scan(/.{1,64}/) - cert = cert.join("\n") - "-----BEGIN CERTIFICATE-----\n#{cert}\n-----END CERTIFICATE-----" + if cert.scan(/BEGIN CERTIFICATE/).length > 1 + formatted_cert = [] + cert.scan(/-{5}BEGIN CERTIFICATE-{5}[\n\r]?.*?-{5}END CERTIFICATE-{5}[\n\r]?/m) do |c| + formatted_cert << format_cert(c) end + formatted_cert.join("\n") + else + cert = cert.gsub(/-{5}\s?(BEGIN|END) CERTIFICATE\s?-{5}/, "") + cert = cert.gsub(/\r/, "") + cert = cert.gsub(/\n/, "") + cert = cert.gsub(/\s/, "") + cert = cert.scan(/.{1,64}/) + cert = cert.join("\n") + "-----BEGIN CERTIFICATE-----\n#{cert}\n-----END CERTIFICATE-----" end + end - # Return a properly formatted private key - # - # @param key [String] The original private key - # @return [String] The formatted private key - # - def self.format_private_key(key) - # don't try to format an encoded private key or if is empty - return key if key.nil? || key.empty? || key.match(/\x0d/) - - # is this an rsa key? - rsa_key = key.match("RSA PRIVATE KEY") - key = key.gsub(/-{5}\s?(BEGIN|END)( RSA)? PRIVATE KEY\s?-{5}/, "") - key = key.gsub(/\n/, "") - key = key.gsub(/\r/, "") - key = key.gsub(/\s/, "") - key = key.scan(/.{1,64}/) - key = key.join("\n") - key_label = rsa_key ? "RSA PRIVATE KEY" : "PRIVATE KEY" - "-----BEGIN #{key_label}-----\n#{key}\n-----END #{key_label}-----" - end + # Return a properly formatted private key + # + # @param key [String] The original private key + # @return [String] The formatted private key + # + def self.format_private_key(key) + # don't try to format an encoded private key or if is empty + return key if key.nil? || key.empty? || key.match(/\x0d/) + + # is this an rsa key? + rsa_key = key.match("RSA PRIVATE KEY") + key = key.gsub(/-{5}\s?(BEGIN|END)( RSA)? PRIVATE KEY\s?-{5}/, "") + key = key.gsub(/\n/, "") + key = key.gsub(/\r/, "") + key = key.gsub(/\s/, "") + key = key.scan(/.{1,64}/) + key = key.join("\n") + key_label = rsa_key ? "RSA PRIVATE KEY" : "PRIVATE KEY" + "-----BEGIN #{key_label}-----\n#{key}\n-----END #{key_label}-----" + end - # Given a certificate string, return an OpenSSL::X509::Certificate object. - # - # @param cert [String] The original certificate - # @return [OpenSSL::X509::Certificate] The certificate object - # - def self.build_cert_object(cert) - return nil if cert.nil? || cert.empty? + # Given a certificate string, return an OpenSSL::X509::Certificate object. + # + # @param cert [String] The original certificate + # @return [OpenSSL::X509::Certificate] The certificate object + # + def self.build_cert_object(cert) + return nil if cert.nil? || cert.empty? - OpenSSL::X509::Certificate.new(format_cert(cert)) - end + OpenSSL::X509::Certificate.new(format_cert(cert)) + end - # Given a private key string, return an OpenSSL::PKey::RSA object. - # - # @param cert [String] The original private key - # @return [OpenSSL::PKey::RSA] The private key object - # - def self.build_private_key_object(private_key) - return nil if private_key.nil? || private_key.empty? + # Given a private key string, return an OpenSSL::PKey::RSA object. + # + # @param cert [String] The original private key + # @return [OpenSSL::PKey::RSA] The private key object + # + def self.build_private_key_object(private_key) + return nil if private_key.nil? || private_key.empty? - OpenSSL::PKey::RSA.new(format_private_key(private_key)) - end + OpenSSL::PKey::RSA.new(format_private_key(private_key)) + end - # Build the Query String signature that will be used in the HTTP-Redirect binding - # to generate the Signature - # @param params [Hash] Parameters to build the Query String - # @option params [String] :type 'SAMLRequest' or 'SAMLResponse' - # @option params [String] :data Base64 encoded SAMLRequest or SAMLResponse - # @option params [String] :relay_state The RelayState parameter - # @option params [String] :sig_alg The SigAlg parameter - # @return [String] The Query String - # - def self.build_query(params) - type, data, relay_state, sig_alg = %i[type data relay_state sig_alg].map { |k| params[k]} - - url_string = +"#{type}=#{CGI.escape(data)}" - url_string << "&RelayState=#{CGI.escape(relay_state)}" if relay_state - url_string << "&SigAlg=#{CGI.escape(sig_alg)}" - end + # Build the Query String signature that will be used in the HTTP-Redirect binding + # to generate the Signature + # @param params [Hash] Parameters to build the Query String + # @option params [String] :type 'SAMLRequest' or 'SAMLResponse' + # @option params [String] :data Base64 encoded SAMLRequest or SAMLResponse + # @option params [String] :relay_state The RelayState parameter + # @option params [String] :sig_alg The SigAlg parameter + # @return [String] The Query String + # + def self.build_query(params) + type, data, relay_state, sig_alg = %i[type data relay_state sig_alg].map { |k| params[k]} - # Reconstruct a canonical query string from raw URI-encoded parts, to be used in verifying a signature - # - # @param params [Hash] Parameters to build the Query String - # @option params [String] :type 'SAMLRequest' or 'SAMLResponse' - # @option params [String] :raw_data URI-encoded, base64 encoded SAMLRequest or SAMLResponse, as sent by IDP - # @option params [String] :raw_relay_state URI-encoded RelayState parameter, as sent by IDP - # @option params [String] :raw_sig_alg URI-encoded SigAlg parameter, as sent by IDP - # @return [String] The Query String - # - def self.build_query_from_raw_parts(params) - type, raw_data, raw_relay_state, raw_sig_alg = %i[type raw_data raw_relay_state raw_sig_alg].map { |k| params[k]} - - url_string = +"#{type}=#{raw_data}" - url_string << "&RelayState=#{raw_relay_state}" if raw_relay_state - url_string << "&SigAlg=#{raw_sig_alg}" - end + url_string = +"#{type}=#{CGI.escape(data)}" + url_string << "&RelayState=#{CGI.escape(relay_state)}" if relay_state + url_string << "&SigAlg=#{CGI.escape(sig_alg)}" + end - # Prepare raw GET parameters (build them from normal parameters - # if not provided). - # - # @param rawparams [Hash] Raw GET Parameters - # @param params [Hash] GET Parameters - # @param lowercase_url_encoding [bool] Lowercase URL Encoding (For ADFS urlencode compatiblity) - # @return [Hash] New raw parameters - # - def self.prepare_raw_get_params(rawparams, params, lowercase_url_encoding=false) - rawparams ||= {} - - if rawparams['SAMLRequest'].nil? && !params['SAMLRequest'].nil? - rawparams['SAMLRequest'] = escape_request_param(params['SAMLRequest'], lowercase_url_encoding) - end - if rawparams['SAMLResponse'].nil? && !params['SAMLResponse'].nil? - rawparams['SAMLResponse'] = escape_request_param(params['SAMLResponse'], lowercase_url_encoding) - end - if rawparams['RelayState'].nil? && !params['RelayState'].nil? - rawparams['RelayState'] = escape_request_param(params['RelayState'], lowercase_url_encoding) - end - if rawparams['SigAlg'].nil? && !params['SigAlg'].nil? - rawparams['SigAlg'] = escape_request_param(params['SigAlg'], lowercase_url_encoding) - end + # Reconstruct a canonical query string from raw URI-encoded parts, to be used in verifying a signature + # + # @param params [Hash] Parameters to build the Query String + # @option params [String] :type 'SAMLRequest' or 'SAMLResponse' + # @option params [String] :raw_data URI-encoded, base64 encoded SAMLRequest or SAMLResponse, as sent by IDP + # @option params [String] :raw_relay_state URI-encoded RelayState parameter, as sent by IDP + # @option params [String] :raw_sig_alg URI-encoded SigAlg parameter, as sent by IDP + # @return [String] The Query String + # + def self.build_query_from_raw_parts(params) + type, raw_data, raw_relay_state, raw_sig_alg = %i[type raw_data raw_relay_state raw_sig_alg].map { |k| params[k]} - rawparams - end + url_string = +"#{type}=#{raw_data}" + url_string << "&RelayState=#{raw_relay_state}" if raw_relay_state + url_string << "&SigAlg=#{raw_sig_alg}" + end - def self.escape_request_param(param, lowercase_url_encoding) - CGI.escape(param).tap do |escaped| - next unless lowercase_url_encoding + # Prepare raw GET parameters (build them from normal parameters + # if not provided). + # + # @param rawparams [Hash] Raw GET Parameters + # @param params [Hash] GET Parameters + # @param lowercase_url_encoding [bool] Lowercase URL Encoding (For ADFS urlencode compatiblity) + # @return [Hash] New raw parameters + # + def self.prepare_raw_get_params(rawparams, params, lowercase_url_encoding=false) + rawparams ||= {} - escaped.gsub!(/%[A-Fa-f0-9]{2}/, &:downcase) - end + if rawparams['SAMLRequest'].nil? && !params['SAMLRequest'].nil? + rawparams['SAMLRequest'] = escape_request_param(params['SAMLRequest'], lowercase_url_encoding) end - - # Validate the Signature parameter sent on the HTTP-Redirect binding - # @param params [Hash] Parameters to be used in the validation process - # @option params [OpenSSL::X509::Certificate] cert The IDP public certificate - # @option params [String] sig_alg The SigAlg parameter - # @option params [String] signature The Signature parameter (base64 encoded) - # @option params [String] query_string The full GET Query String to be compared - # @return [Boolean] True if the Signature is valid, False otherwise - # - def self.verify_signature(params) - cert, sig_alg, signature, query_string = %i[cert sig_alg signature query_string].map { |k| params[k]} - signature_algorithm = XMLSecurity::BaseDocument.new.algorithm(sig_alg) - cert.public_key.verify(signature_algorithm.new, Base64.decode64(signature), query_string) + if rawparams['SAMLResponse'].nil? && !params['SAMLResponse'].nil? + rawparams['SAMLResponse'] = escape_request_param(params['SAMLResponse'], lowercase_url_encoding) + end + if rawparams['RelayState'].nil? && !params['RelayState'].nil? + rawparams['RelayState'] = escape_request_param(params['RelayState'], lowercase_url_encoding) + end + if rawparams['SigAlg'].nil? && !params['SigAlg'].nil? + rawparams['SigAlg'] = escape_request_param(params['SigAlg'], lowercase_url_encoding) end - # Build the status error message - # @param status_code [String] StatusCode value - # @param status_message [Strig] StatusMessage value - # @return [String] The status error message - def self.status_error_msg(error_msg, raw_status_code = nil, status_message = nil) - unless raw_status_code.nil? - if raw_status_code.include?("|") - status_codes = raw_status_code.split(' | ') - values = status_codes.collect do |status_code| - status_code.split(':').last - end - printable_code = values.join(" => ") - else - printable_code = raw_status_code.split(':').last - end - error_msg += ", was #{printable_code}" - end + rawparams + end - error_msg += " -> #{status_message}" unless status_message.nil? + def self.escape_request_param(param, lowercase_url_encoding) + CGI.escape(param).tap do |escaped| + next unless lowercase_url_encoding - error_msg + escaped.gsub!(/%[A-Fa-f0-9]{2}/, &:downcase) end + end - # Obtains the decrypted string from an Encrypted node element in XML, - # given multiple private keys to try. - # @param encrypted_node [REXML::Element] The Encrypted element - # @param private_keys [Array] The Service provider private key - # @return [String] The decrypted data - def self.decrypt_multi(encrypted_node, private_keys) - raise ArgumentError.new('private_keys must be specified') if !private_keys || private_keys.empty? - - error = nil - private_keys.each do |key| - begin - return decrypt_data(encrypted_node, key) - rescue OpenSSL::PKey::PKeyError => e - error ||= e + # Validate the Signature parameter sent on the HTTP-Redirect binding + # @param params [Hash] Parameters to be used in the validation process + # @option params [OpenSSL::X509::Certificate] cert The IDP public certificate + # @option params [String] sig_alg The SigAlg parameter + # @option params [String] signature The Signature parameter (base64 encoded) + # @option params [String] query_string The full GET Query String to be compared + # @return [Boolean] True if the Signature is valid, False otherwise + # + def self.verify_signature(params) + cert, sig_alg, signature, query_string = %i[cert sig_alg signature query_string].map { |k| params[k]} + signature_algorithm = XMLSecurity::BaseDocument.new.algorithm(sig_alg) + cert.public_key.verify(signature_algorithm.new, Base64.decode64(signature), query_string) + end + + # Build the status error message + # @param status_code [String] StatusCode value + # @param status_message [Strig] StatusMessage value + # @return [String] The status error message + def self.status_error_msg(error_msg, raw_status_code = nil, status_message = nil) + unless raw_status_code.nil? + if raw_status_code.include?("|") + status_codes = raw_status_code.split(' | ') + values = status_codes.collect do |status_code| + status_code.split(':').last end + printable_code = values.join(" => ") + else + printable_code = raw_status_code.split(':').last end - - raise(error) if error + error_msg += ", was #{printable_code}" end - # Obtains the decrypted string from an Encrypted node element in XML - # @param encrypted_node [REXML::Element] The Encrypted element - # @param private_key [OpenSSL::PKey::RSA] The Service provider private key - # @return [String] The decrypted data - def self.decrypt_data(encrypted_node, private_key) - encrypt_data = REXML::XPath.first( - encrypted_node, - "./xenc:EncryptedData", - { 'xenc' => XENC } - ) - symmetric_key = retrieve_symmetric_key(encrypt_data, private_key) - cipher_value = REXML::XPath.first( - encrypt_data, - "./xenc:CipherData/xenc:CipherValue", - { 'xenc' => XENC } - ) - node = Base64.decode64(element_text(cipher_value)) - encrypt_method = REXML::XPath.first( - encrypt_data, - "./xenc:EncryptionMethod", - { 'xenc' => XENC } - ) - algorithm = encrypt_method.attributes['Algorithm'] - retrieve_plaintext(node, symmetric_key, algorithm) - end + error_msg += " -> #{status_message}" unless status_message.nil? - # Obtains the symmetric key from the EncryptedData element - # @param encrypt_data [REXML::Element] The EncryptedData element - # @param private_key [OpenSSL::PKey::RSA] The Service provider private key - # @return [String] The symmetric key - def self.retrieve_symmetric_key(encrypt_data, private_key) - encrypted_key = REXML::XPath.first( - encrypt_data, - "./ds:KeyInfo/xenc:EncryptedKey | ./KeyInfo/xenc:EncryptedKey | //xenc:EncryptedKey[@Id=$id]", - { "ds" => DSIG, "xenc" => XENC }, - { "id" => retrieve_symetric_key_reference(encrypt_data) } - ) - - encrypted_symmetric_key_element = REXML::XPath.first( - encrypted_key, - "./xenc:CipherData/xenc:CipherValue", - "xenc" => XENC - ) - - cipher_text = Base64.decode64(element_text(encrypted_symmetric_key_element)) - - encrypt_method = REXML::XPath.first( - encrypted_key, - "./xenc:EncryptionMethod", - "xenc" => XENC - ) - - algorithm = encrypt_method.attributes['Algorithm'] - retrieve_plaintext(cipher_text, private_key, algorithm) - end + error_msg + end - def self.retrieve_symetric_key_reference(encrypt_data) - REXML::XPath.first( - encrypt_data, - "substring-after(./ds:KeyInfo/ds:RetrievalMethod/@URI, '#')", - { "ds" => DSIG } - ) + # Obtains the decrypted string from an Encrypted node element in XML, + # given multiple private keys to try. + # @param encrypted_node [REXML::Element] The Encrypted element + # @param private_keys [Array] The Service provider private key + # @return [String] The decrypted data + def self.decrypt_multi(encrypted_node, private_keys) + raise ArgumentError.new('private_keys must be specified') if !private_keys || private_keys.empty? + + error = nil + private_keys.each do |key| + begin + return decrypt_data(encrypted_node, key) + rescue OpenSSL::PKey::PKeyError => e + error ||= e + end end - # Obtains the deciphered text - # @param cipher_text [String] The ciphered text - # @param symmetric_key [String] The symmetric key used to encrypt the text - # @param algorithm [String] The encrypted algorithm - # @return [String] The deciphered text - def self.retrieve_plaintext(cipher_text, symmetric_key, algorithm) - case algorithm - when 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc' then cipher = OpenSSL::Cipher.new('DES-EDE3-CBC').decrypt - when 'http://www.w3.org/2001/04/xmlenc#aes128-cbc' then cipher = OpenSSL::Cipher.new('AES-128-CBC').decrypt - when 'http://www.w3.org/2001/04/xmlenc#aes192-cbc' then cipher = OpenSSL::Cipher.new('AES-192-CBC').decrypt - when 'http://www.w3.org/2001/04/xmlenc#aes256-cbc' then cipher = OpenSSL::Cipher.new('AES-256-CBC').decrypt - when 'http://www.w3.org/2009/xmlenc11#aes128-gcm' then auth_cipher = OpenSSL::Cipher.new('aes-128-gcm').decrypt - when 'http://www.w3.org/2009/xmlenc11#aes192-gcm' then auth_cipher = OpenSSL::Cipher.new('aes-192-gcm').decrypt - when 'http://www.w3.org/2009/xmlenc11#aes256-gcm' then auth_cipher = OpenSSL::Cipher.new('aes-256-gcm').decrypt - when 'http://www.w3.org/2001/04/xmlenc#rsa-1_5' then rsa = symmetric_key - when 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p' then oaep = symmetric_key - end + raise(error) if error + end - if cipher - iv_len = cipher.iv_len - data = cipher_text[iv_len..] - cipher.padding = 0 - cipher.key = symmetric_key - cipher.iv = cipher_text[0..iv_len-1] - assertion_plaintext = cipher.update(data) - assertion_plaintext << cipher.final - elsif auth_cipher - iv_len = auth_cipher.iv_len - text_len = cipher_text.length - tag_len = 16 - data = cipher_text[iv_len..text_len-1-tag_len] - auth_cipher.padding = 0 - auth_cipher.key = symmetric_key - auth_cipher.iv = cipher_text[0..iv_len-1] - auth_cipher.auth_data = '' - auth_cipher.auth_tag = cipher_text[text_len-tag_len..] - assertion_plaintext = auth_cipher.update(data) - assertion_plaintext << auth_cipher.final - elsif rsa - rsa.private_decrypt(cipher_text) - elsif oaep - oaep.private_decrypt(cipher_text, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING) - else - cipher_text - end - end + # Obtains the decrypted string from an Encrypted node element in XML + # @param encrypted_node [REXML::Element] The Encrypted element + # @param private_key [OpenSSL::PKey::RSA] The Service provider private key + # @return [String] The decrypted data + def self.decrypt_data(encrypted_node, private_key) + encrypt_data = REXML::XPath.first( + encrypted_node, + "./xenc:EncryptedData", + { 'xenc' => XENC } + ) + symmetric_key = retrieve_symmetric_key(encrypt_data, private_key) + cipher_value = REXML::XPath.first( + encrypt_data, + "./xenc:CipherData/xenc:CipherValue", + { 'xenc' => XENC } + ) + node = Base64.decode64(element_text(cipher_value)) + encrypt_method = REXML::XPath.first( + encrypt_data, + "./xenc:EncryptionMethod", + { 'xenc' => XENC } + ) + algorithm = encrypt_method.attributes['Algorithm'] + retrieve_plaintext(node, symmetric_key, algorithm) + end + + # Obtains the symmetric key from the EncryptedData element + # @param encrypt_data [REXML::Element] The EncryptedData element + # @param private_key [OpenSSL::PKey::RSA] The Service provider private key + # @return [String] The symmetric key + def self.retrieve_symmetric_key(encrypt_data, private_key) + encrypted_key = REXML::XPath.first( + encrypt_data, + "./ds:KeyInfo/xenc:EncryptedKey | ./KeyInfo/xenc:EncryptedKey | //xenc:EncryptedKey[@Id=$id]", + { "ds" => DSIG, "xenc" => XENC }, + { "id" => retrieve_symetric_key_reference(encrypt_data) } + ) + + encrypted_symmetric_key_element = REXML::XPath.first( + encrypted_key, + "./xenc:CipherData/xenc:CipherValue", + "xenc" => XENC + ) + + cipher_text = Base64.decode64(element_text(encrypted_symmetric_key_element)) + + encrypt_method = REXML::XPath.first( + encrypted_key, + "./xenc:EncryptionMethod", + "xenc" => XENC + ) + + algorithm = encrypt_method.attributes['Algorithm'] + retrieve_plaintext(cipher_text, private_key, algorithm) + end + + def self.retrieve_symetric_key_reference(encrypt_data) + REXML::XPath.first( + encrypt_data, + "substring-after(./ds:KeyInfo/ds:RetrievalMethod/@URI, '#')", + { "ds" => DSIG } + ) + end - def self.set_prefix(value) - UUID_PREFIX.replace value + # Obtains the deciphered text + # @param cipher_text [String] The ciphered text + # @param symmetric_key [String] The symmetric key used to encrypt the text + # @param algorithm [String] The encrypted algorithm + # @return [String] The deciphered text + def self.retrieve_plaintext(cipher_text, symmetric_key, algorithm) + case algorithm + when 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc' then cipher = OpenSSL::Cipher.new('DES-EDE3-CBC').decrypt + when 'http://www.w3.org/2001/04/xmlenc#aes128-cbc' then cipher = OpenSSL::Cipher.new('AES-128-CBC').decrypt + when 'http://www.w3.org/2001/04/xmlenc#aes192-cbc' then cipher = OpenSSL::Cipher.new('AES-192-CBC').decrypt + when 'http://www.w3.org/2001/04/xmlenc#aes256-cbc' then cipher = OpenSSL::Cipher.new('AES-256-CBC').decrypt + when 'http://www.w3.org/2009/xmlenc11#aes128-gcm' then auth_cipher = OpenSSL::Cipher.new('aes-128-gcm').decrypt + when 'http://www.w3.org/2009/xmlenc11#aes192-gcm' then auth_cipher = OpenSSL::Cipher.new('aes-192-gcm').decrypt + when 'http://www.w3.org/2009/xmlenc11#aes256-gcm' then auth_cipher = OpenSSL::Cipher.new('aes-256-gcm').decrypt + when 'http://www.w3.org/2001/04/xmlenc#rsa-1_5' then rsa = symmetric_key + when 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p' then oaep = symmetric_key end - def self.uuid - "#{UUID_PREFIX}#{SecureRandom.uuid}" + if cipher + iv_len = cipher.iv_len + data = cipher_text[iv_len..] + cipher.padding = 0 + cipher.key = symmetric_key + cipher.iv = cipher_text[0..iv_len-1] + assertion_plaintext = cipher.update(data) + assertion_plaintext << cipher.final + elsif auth_cipher + iv_len = auth_cipher.iv_len + text_len = cipher_text.length + tag_len = 16 + data = cipher_text[iv_len..text_len-1-tag_len] + auth_cipher.padding = 0 + auth_cipher.key = symmetric_key + auth_cipher.iv = cipher_text[0..iv_len-1] + auth_cipher.auth_data = '' + auth_cipher.auth_tag = cipher_text[text_len-tag_len..] + assertion_plaintext = auth_cipher.update(data) + assertion_plaintext << auth_cipher.final + elsif rsa + rsa.private_decrypt(cipher_text) + elsif oaep + oaep.private_decrypt(cipher_text, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING) + else + cipher_text end + end - # Given two strings, attempt to match them as URIs using Rails' parse method. If they can be parsed, - # then the fully-qualified domain name and the host should performa a case-insensitive match, per the - # RFC for URIs. If Rails can not parse the string in to URL pieces, return a boolean match of the - # two strings. This maintains the previous functionality. - # @return [Boolean] - def self.uri_match?(destination_url, settings_url) - dest_uri = URI.parse(destination_url) - acs_uri = URI.parse(settings_url) - - if dest_uri.scheme.nil? || acs_uri.scheme.nil? || dest_uri.host.nil? || acs_uri.host.nil? - raise URI::InvalidURIError - end + def self.set_prefix(value) + UUID_PREFIX.replace value + end - dest_uri.scheme.casecmp(acs_uri.scheme) == 0 && - dest_uri.host.casecmp(acs_uri.host) == 0 && - dest_uri.path == acs_uri.path && - dest_uri.query == acs_uri.query - rescue URI::InvalidURIError - original_uri_match?(destination_url, settings_url) - end + def self.uuid + "#{UUID_PREFIX}#{SecureRandom.uuid}" + end - # If Rails' URI.parse can't match to valid URL, default back to the original matching service. - # @return [Boolean] - def self.original_uri_match?(destination_url, settings_url) - destination_url == settings_url + # Given two strings, attempt to match them as URIs using Rails' parse method. If they can be parsed, + # then the fully-qualified domain name and the host should performa a case-insensitive match, per the + # RFC for URIs. If Rails can not parse the string in to URL pieces, return a boolean match of the + # two strings. This maintains the previous functionality. + # @return [Boolean] + def self.uri_match?(destination_url, settings_url) + dest_uri = URI.parse(destination_url) + acs_uri = URI.parse(settings_url) + + if dest_uri.scheme.nil? || acs_uri.scheme.nil? || dest_uri.host.nil? || acs_uri.host.nil? + raise URI::InvalidURIError end - # Given a REXML::Element instance, return the concatenation of all child text nodes. Assumes - # that there all children other than text nodes can be ignored (e.g. comments). If nil is - # passed, nil will be returned. - def self.element_text(element) - element.texts.map(&:value).join if element - end + dest_uri.scheme.casecmp(acs_uri.scheme) == 0 && + dest_uri.host.casecmp(acs_uri.host) == 0 && + dest_uri.path == acs_uri.path && + dest_uri.query == acs_uri.query + rescue URI::InvalidURIError + original_uri_match?(destination_url, settings_url) + end + + # If Rails' URI.parse can't match to valid URL, default back to the original matching service. + # @return [Boolean] + def self.original_uri_match?(destination_url, settings_url) + destination_url == settings_url + end + + # Given a REXML::Element instance, return the concatenation of all child text nodes. Assumes + # that there all children other than text nodes can be ignored (e.g. comments). If nil is + # passed, nil will be returned. + def self.element_text(element) + element.texts.map(&:value).join if element end end end diff --git a/lib/onelogin/ruby-saml/validation_error.rb b/lib/onelogin/ruby-saml/validation_error.rb index fbe37f9d5..9fcf287eb 100644 --- a/lib/onelogin/ruby-saml/validation_error.rb +++ b/lib/onelogin/ruby-saml/validation_error.rb @@ -1,9 +1,6 @@ # frozen_string_literal: true -module OneLogin - module RubySaml - class ValidationError < StandardError - end +module RubySaml + class ValidationError < StandardError end end - diff --git a/lib/onelogin/ruby-saml/version.rb b/lib/onelogin/ruby-saml/version.rb index 1f9866ef9..3c8e9a465 100644 --- a/lib/onelogin/ruby-saml/version.rb +++ b/lib/onelogin/ruby-saml/version.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -module OneLogin - module RubySaml - VERSION = '1.16.0' - end +module RubySaml + VERSION = '2.0.0' end diff --git a/lib/xml_security.rb b/lib/xml_security.rb index 9706b8be3..e55a62686 100644 --- a/lib/xml_security.rb +++ b/lib/xml_security.rb @@ -183,7 +183,7 @@ def compute_digest(document, digest_algorithm) end class SignedDocument < BaseDocument - include OneLogin::RubySaml::ErrorHandling + include RubySaml::ErrorHandling attr_writer :signed_element_id @@ -205,7 +205,7 @@ def validate_document(idp_cert_fingerprint, soft = true, options = {}) ) if cert_element - base64_cert = OneLogin::RubySaml::Utils.element_text(cert_element) + base64_cert = RubySaml::Utils.element_text(cert_element) cert_text = Base64.decode64(base64_cert) begin cert = OpenSSL::X509::Certificate.new(cert_text) @@ -243,7 +243,7 @@ def validate_document_with_cert(idp_cert, soft = true) ) if cert_element - base64_cert = OneLogin::RubySaml::Utils.element_text(cert_element) + base64_cert = RubySaml::Utils.element_text(cert_element) cert_text = Base64.decode64(base64_cert) begin cert = OpenSSL::X509::Certificate.new(cert_text) @@ -290,7 +290,7 @@ def validate_signature(base64_cert, soft = true) "./ds:SignatureValue", {"ds" => DSIG} ) - signature = Base64.decode64(OneLogin::RubySaml::Utils.element_text(base64_signature)) + signature = Base64.decode64(RubySaml::Utils.element_text(base64_signature)) # canonicalization method canon_algorithm = canon_algorithm REXML::XPath.first( @@ -334,7 +334,7 @@ def validate_signature(base64_cert, soft = true) "//ds:DigestValue", { "ds" => DSIG } ) - digest_value = Base64.decode64(OneLogin::RubySaml::Utils.element_text(encoded_digest_value)) + digest_value = Base64.decode64(RubySaml::Utils.element_text(encoded_digest_value)) unless digests_match?(hash, digest_value) return append_error("Digest mismatch", soft) diff --git a/ruby-saml.gemspec b/ruby-saml.gemspec index 5383b56d2..013d9c75b 100644 --- a/ruby-saml.gemspec +++ b/ruby-saml.gemspec @@ -3,7 +3,7 @@ require 'onelogin/ruby-saml/version' Gem::Specification.new do |s| s.name = 'ruby-saml' - s.version = OneLogin::RubySaml::VERSION + s.version = RubySaml::VERSION s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["SAML Toolkit", "Sixto Martin"] diff --git a/test/attributes_test.rb b/test/attributes_test.rb index 50d1a1015..a765044f6 100644 --- a/test/attributes_test.rb +++ b/test/attributes_test.rb @@ -5,7 +5,7 @@ class AttributesTest < Minitest::Test describe 'Attributes' do let(:attributes) do - OneLogin::RubySaml::Attributes.new({ + RubySaml::Attributes.new({ 'email' => ['tom@hanks.com'], 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname' => ['Tom'], 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname' => ['Hanks'] diff --git a/test/idp_metadata_parser_test.rb b/test/idp_metadata_parser_test.rb index d1c5e3fcc..08e985c6b 100644 --- a/test/idp_metadata_parser_test.rb +++ b/test/idp_metadata_parser_test.rb @@ -19,7 +19,7 @@ def initialize; end describe "parsing an IdP descriptor file" do it "extract settings details from xml" do - idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + idp_metadata_parser = RubySaml::IdpMetadataParser.new settings = idp_metadata_parser.parse(idp_metadata_descriptor) @@ -35,14 +35,14 @@ def initialize; end end it "extract certificate from md:KeyDescriptor[@use='signing']" do - idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + idp_metadata_parser = RubySaml::IdpMetadataParser.new idp_metadata = idp_metadata_descriptor settings = idp_metadata_parser.parse(idp_metadata) assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", settings.idp_cert_fingerprint end it "extract certificate from md:KeyDescriptor[@use='encryption']" do - idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + idp_metadata_parser = RubySaml::IdpMetadataParser.new idp_metadata = idp_metadata_descriptor idp_metadata = idp_metadata.sub(/(.*?)<\/md:KeyDescriptor>/m, "") settings = idp_metadata_parser.parse(idp_metadata) @@ -50,7 +50,7 @@ def initialize; end end it "extract certificate from md:KeyDescriptor" do - idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + idp_metadata_parser = RubySaml::IdpMetadataParser.new idp_metadata = idp_metadata_descriptor idp_metadata = idp_metadata.sub(/(.*?)<\/md:KeyDescriptor>/m, "") idp_metadata = idp_metadata.sub('', '') @@ -59,7 +59,7 @@ def initialize; end end it "extract SSO endpoint with no specific binding, it takes the first" do - idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + idp_metadata_parser = RubySaml::IdpMetadataParser.new idp_metadata = idp_metadata_descriptor3 settings = idp_metadata_parser.parse(idp_metadata) assert_equal "https://idp.example.com/idp/profile/Shibboleth/SSO", settings.idp_sso_service_url @@ -67,7 +67,7 @@ def initialize; end end it "extract SSO endpoint with specific binding as a String" do - idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + idp_metadata_parser = RubySaml::IdpMetadataParser.new idp_metadata = idp_metadata_descriptor3 options = {} options[:sso_binding] = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' @@ -87,7 +87,7 @@ def initialize; end end it "extract SSO endpoint with specific binding as an Array" do - idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + idp_metadata_parser = RubySaml::IdpMetadataParser.new idp_metadata = idp_metadata_descriptor3 options = {} options[:sso_binding] = ['urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'] @@ -115,14 +115,14 @@ def initialize; end end it "extract NameIDFormat no specific priority, it takes the first" do - idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + idp_metadata_parser = RubySaml::IdpMetadataParser.new idp_metadata = idp_metadata_descriptor3 settings = idp_metadata_parser.parse(idp_metadata) assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", settings.name_identifier_format end it "extract NameIDFormat specific priority as a String" do - idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + idp_metadata_parser = RubySaml::IdpMetadataParser.new idp_metadata = idp_metadata_descriptor3 options = {} options[:name_id_format] = 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified' @@ -139,7 +139,7 @@ def initialize; end end it "extract NameIDFormat specific priority as an Array" do - idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + idp_metadata_parser = RubySaml::IdpMetadataParser.new idp_metadata = idp_metadata_descriptor3 options = {} options[:name_id_format] = ['urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'] @@ -152,7 +152,7 @@ def initialize; end end it "uses settings options as hash for overrides" do - idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + idp_metadata_parser = RubySaml::IdpMetadataParser.new idp_metadata = idp_metadata_descriptor settings = idp_metadata_parser.parse(idp_metadata, { :settings => { @@ -168,12 +168,12 @@ def initialize; end end it "merges results into given settings object" do - settings = OneLogin::RubySaml::Settings.new(:security => { + settings = RubySaml::Settings.new(:security => { :digest_method => XMLSecurity::Document::SHA256, :signature_method => XMLSecurity::Document::RSA_SHA256 }) - OneLogin::RubySaml::IdpMetadataParser.new.parse(idp_metadata_descriptor, :settings => settings) + RubySaml::IdpMetadataParser.new.parse(idp_metadata_descriptor, :settings => settings) assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", settings.idp_cert_fingerprint assert_equal XMLSecurity::Document::SHA256, settings.security[:digest_method] @@ -183,7 +183,7 @@ def initialize; end describe "parsing an IdP descriptor file into an Hash" do it "extract settings details from xml" do - idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + idp_metadata_parser = RubySaml::IdpMetadataParser.new metadata = idp_metadata_parser.parse_to_hash(idp_metadata_descriptor) @@ -199,14 +199,14 @@ def initialize; end end it "extract certificate from md:KeyDescriptor[@use='signing']" do - idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + idp_metadata_parser = RubySaml::IdpMetadataParser.new idp_metadata = idp_metadata_descriptor metadata = idp_metadata_parser.parse_to_hash(idp_metadata) assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", metadata[:idp_cert_fingerprint] end it "extract certificate from md:KeyDescriptor[@use='encryption']" do - idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + idp_metadata_parser = RubySaml::IdpMetadataParser.new idp_metadata = idp_metadata_descriptor idp_metadata = idp_metadata.sub(/(.*?)<\/md:KeyDescriptor>/m, "") parsed_metadata = idp_metadata_parser.parse_to_hash(idp_metadata) @@ -214,7 +214,7 @@ def initialize; end end it "extract certificate from md:KeyDescriptor" do - idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + idp_metadata_parser = RubySaml::IdpMetadataParser.new idp_metadata = idp_metadata_descriptor idp_metadata = idp_metadata.sub(/(.*?)<\/md:KeyDescriptor>/m, "") idp_metadata = idp_metadata.sub('', '') @@ -223,7 +223,7 @@ def initialize; end end it "extract SSO endpoint with no specific binding, it takes the first" do - idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + idp_metadata_parser = RubySaml::IdpMetadataParser.new idp_metadata = idp_metadata_descriptor3 metadata = idp_metadata_parser.parse_to_hash(idp_metadata) assert_equal "https://idp.example.com/idp/profile/Shibboleth/SSO", metadata[:idp_sso_service_url] @@ -231,7 +231,7 @@ def initialize; end end it "extract SSO endpoint with specific binding" do - idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + idp_metadata_parser = RubySaml::IdpMetadataParser.new idp_metadata = idp_metadata_descriptor3 options = {} options[:sso_binding] = ['urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'] @@ -251,7 +251,7 @@ def initialize; end end it "ignores a given :settings hash" do - idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + idp_metadata_parser = RubySaml::IdpMetadataParser.new idp_metadata = idp_metadata_descriptor parsed_metadata = idp_metadata_parser.parse_to_hash(idp_metadata, { :settings => { @@ -266,7 +266,7 @@ def initialize; end end it "can extract certificates multiple times in sequence" do - idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + idp_metadata_parser = RubySaml::IdpMetadataParser.new idp_metadata1 = idp_metadata_descriptor idp_metadata2 = idp_metadata_descriptor4 metadata1 = idp_metadata_parser.parse_to_hash(idp_metadata1) @@ -279,7 +279,7 @@ def initialize; end describe "parsing an IdP descriptor file with multiple signing certs" do it "extract settings details from xml" do - idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + idp_metadata_parser = RubySaml::IdpMetadataParser.new settings = idp_metadata_parser.parse(idp_metadata_descriptor2) @@ -314,7 +314,7 @@ def initialize; end end it "extract settings from remote xml" do - idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + idp_metadata_parser = RubySaml::IdpMetadataParser.new settings = idp_metadata_parser.parse_remote(@url) assert_equal "https://hello.example.com/access/saml/idp.xml", settings.idp_entity_id @@ -330,7 +330,7 @@ def initialize; end end it "accept self signed certificate if insturcted" do - idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + idp_metadata_parser = RubySaml::IdpMetadataParser.new idp_metadata_parser.parse_remote(@url, false) assert_equal OpenSSL::SSL::VERIFY_NONE, @http.verify_mode @@ -350,7 +350,7 @@ def initialize; end end it "extract settings from remote xml" do - idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + idp_metadata_parser = RubySaml::IdpMetadataParser.new parsed_metadata = idp_metadata_parser.parse_remote_to_hash(@url) assert_equal "https://hello.example.com/access/saml/idp.xml", parsed_metadata[:idp_entity_id] @@ -366,7 +366,7 @@ def initialize; end end it "accept self signed certificate if insturcted" do - idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + idp_metadata_parser = RubySaml::IdpMetadataParser.new idp_metadata_parser.parse_remote_to_hash(@url, false) assert_equal OpenSSL::SSL::VERIFY_NONE, @http.verify_mode @@ -375,7 +375,7 @@ def initialize; end describe "download failure cases" do it "raises an exception when the url has no scheme" do - idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + idp_metadata_parser = RubySaml::IdpMetadataParser.new exception = assert_raises(ArgumentError) do idp_metadata_parser.parse_remote("blahblah") @@ -393,9 +393,9 @@ def initialize; end Net::HTTP.expects(:new).returns(@http) @http.expects(:request).returns(mock_response) - idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + idp_metadata_parser = RubySaml::IdpMetadataParser.new - exception = assert_raises(OneLogin::RubySaml::HttpError) do + exception = assert_raises(RubySaml::HttpError) do idp_metadata_parser.parse_remote("https://hello.example.com/access/saml/idp.xml") end @@ -405,7 +405,7 @@ def initialize; end describe "parsing metadata with and without ValidUntil and CacheDuration" do before do - @idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + @idp_metadata_parser = RubySaml::IdpMetadataParser.new end it "if no ValidUntil or CacheDuration return nothing" do @@ -446,7 +446,7 @@ def initialize; end describe "parsing metadata with many entity descriptors" do before do - @idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + @idp_metadata_parser = RubySaml::IdpMetadataParser.new @idp_metadata = idp_metadata_multiple_descriptors2 @settings = @idp_metadata_parser.parse(@idp_metadata) end @@ -487,7 +487,7 @@ def initialize; end describe "parsing metadata with no IDPSSODescriptor element" do before do - @idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + @idp_metadata_parser = RubySaml::IdpMetadataParser.new @idp_metadata = no_idp_metadata_descriptor end @@ -498,7 +498,7 @@ def initialize; end describe "parsing metadata with IDPSSODescriptor with multiple certs" do before do - @idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + @idp_metadata_parser = RubySaml::IdpMetadataParser.new @idp_metadata = idp_metadata_multiple_certs @settings = @idp_metadata_parser.parse(@idp_metadata) end @@ -569,7 +569,7 @@ def initialize; end describe "parsing metadata with IDPSSODescriptor with multiple signing certs" do before do - @idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + @idp_metadata_parser = RubySaml::IdpMetadataParser.new @idp_metadata = idp_metadata_multiple_signing_certs @settings = @idp_metadata_parser.parse(@idp_metadata) end @@ -616,7 +616,7 @@ def initialize; end describe "parsing metadata with IDPSSODescriptor with same signature cert and encrypt cert" do before do - @idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + @idp_metadata_parser = RubySaml::IdpMetadataParser.new @idp_metadata = idp_metadata_same_sign_and_encrypt_cert @settings = @idp_metadata_parser.parse(@idp_metadata) end @@ -662,7 +662,7 @@ def initialize; end describe "parsing metadata with IDPSSODescriptor with different signature cert and encrypt cert" do before do - @idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + @idp_metadata_parser = RubySaml::IdpMetadataParser.new @idp_metadata = idp_metadata_different_sign_and_encrypt_cert @settings = @idp_metadata_parser.parse(@idp_metadata) end @@ -733,7 +733,7 @@ def initialize; end describe "metadata with different singlelogout response location" do it "should return the responselocation if it exists" do - idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + idp_metadata_parser = RubySaml::IdpMetadataParser.new settings = idp_metadata_parser.parse(idp_different_slo_response_location) @@ -743,7 +743,7 @@ def initialize; end end it "should set the responselocation to nil if it doesnt exist" do - idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + idp_metadata_parser = RubySaml::IdpMetadataParser.new settings = idp_metadata_parser.parse(idp_without_slo_response_location) diff --git a/test/logging_test.rb b/test/logging_test.rb index 7006516cb..bd6d9be86 100644 --- a/test/logging_test.rb +++ b/test/logging_test.rb @@ -6,17 +6,17 @@ class LoggingTest < Minitest::Test describe "Logging" do before do - OneLogin::RubySaml::Logging.logger = nil + RubySaml::Logging.logger = nil end after do - OneLogin::RubySaml::Logging.logger = ::TEST_LOGGER + RubySaml::Logging.logger = ::TEST_LOGGER end describe "given no specific logging setup" do it "prints to stdout" do - OneLogin::RubySaml::Logging::DEFAULT_LOGGER.expects(:debug).with('hi mom') - OneLogin::RubySaml::Logging.debug('hi mom') + RubySaml::Logging::DEFAULT_LOGGER.expects(:debug).with('hi mom') + RubySaml::Logging.debug('hi mom') end end @@ -36,26 +36,26 @@ class LoggingTest < Minitest::Test logger.expects(:debug).with('hi mom') logger.expects(:info).with('sup?') - OneLogin::RubySaml::Logging.debug('hi mom') - OneLogin::RubySaml::Logging.info('sup?') + RubySaml::Logging.debug('hi mom') + RubySaml::Logging.info('sup?') end end describe "given a specific Logger" do let(:logger) { mock('Logger') } - before { OneLogin::RubySaml::Logging.logger = logger } + before { RubySaml::Logging.logger = logger } after do - OneLogin::RubySaml::Logging.logger = ::TEST_LOGGER + RubySaml::Logging.logger = ::TEST_LOGGER end it "delegates to the object" do logger.expects(:debug).with('hi mom') logger.expects(:info).with('sup?') - OneLogin::RubySaml::Logging.debug('hi mom') - OneLogin::RubySaml::Logging.info('sup?') + RubySaml::Logging.debug('hi mom') + RubySaml::Logging.info('sup?') end end end diff --git a/test/logout_responses/logoutresponse_fixtures.rb b/test/logout_responses/logoutresponse_fixtures.rb index c1110228f..4db880bcd 100644 --- a/test/logout_responses/logoutresponse_fixtures.rb +++ b/test/logout_responses/logoutresponse_fixtures.rb @@ -71,7 +71,7 @@ def invalid_xml_logout_response_document end def settings - @settings ||= OneLogin::RubySaml::Settings.new( + @settings ||= RubySaml::Settings.new( { :assertion_consumer_service_url => "http://app.muda.no/sso/consume", :single_logout_service_url => "http://app.muda.no/sso/consume_logout", diff --git a/test/logoutrequest_test.rb b/test/logoutrequest_test.rb index 71091953a..0c75071e8 100644 --- a/test/logoutrequest_test.rb +++ b/test/logoutrequest_test.rb @@ -5,7 +5,7 @@ class RequestTest < Minitest::Test describe "Logoutrequest" do - let(:settings) { OneLogin::RubySaml::Settings.new } + let(:settings) { RubySaml::Settings.new } before do settings.idp_slo_service_url = "http://unauth.com/logout" @@ -13,7 +13,7 @@ class RequestTest < Minitest::Test end it "create the deflated SAMLRequest URL parameter" do - unauth_url = OneLogin::RubySaml::Logoutrequest.new.create(settings) + unauth_url = RubySaml::Logoutrequest.new.create(settings) assert_match(/^http:\/\/unauth\.com\/logout\?SAMLRequest=/, unauth_url) inflated = decode_saml_request_payload(unauth_url) @@ -21,33 +21,33 @@ class RequestTest < Minitest::Test end it "support additional params" do - unauth_url = OneLogin::RubySaml::Logoutrequest.new.create(settings, { :hello => nil }) + unauth_url = RubySaml::Logoutrequest.new.create(settings, { :hello => nil }) assert_match(/&hello=$/, unauth_url) - unauth_url = OneLogin::RubySaml::Logoutrequest.new.create(settings, { :foo => "bar" }) + unauth_url = RubySaml::Logoutrequest.new.create(settings, { :foo => "bar" }) assert_match(/&foo=bar$/, unauth_url) end it "RelayState cases" do - unauth_url = OneLogin::RubySaml::Logoutrequest.new.create(settings, { :RelayState => nil }) + unauth_url = RubySaml::Logoutrequest.new.create(settings, { :RelayState => nil }) assert !unauth_url.include?('RelayState') - unauth_url = OneLogin::RubySaml::Logoutrequest.new.create(settings, { :RelayState => "http://example.com" }) + unauth_url = RubySaml::Logoutrequest.new.create(settings, { :RelayState => "http://example.com" }) assert unauth_url.include?('&RelayState=http%3A%2F%2Fexample.com') - unauth_url = OneLogin::RubySaml::Logoutrequest.new.create(settings, { 'RelayState' => nil }) + unauth_url = RubySaml::Logoutrequest.new.create(settings, { 'RelayState' => nil }) assert !unauth_url.include?('RelayState') - unauth_url = OneLogin::RubySaml::Logoutrequest.new.create(settings, { 'RelayState' => "http://example.com" }) + unauth_url = RubySaml::Logoutrequest.new.create(settings, { 'RelayState' => "http://example.com" }) assert unauth_url.include?('&RelayState=http%3A%2F%2Fexample.com') end it "set sessionindex" do settings.idp_slo_service_url = "http://example.com" - sessionidx = OneLogin::RubySaml::Utils.uuid + sessionidx = RubySaml::Utils.uuid settings.sessionindex = sessionidx - unauth_url = OneLogin::RubySaml::Logoutrequest.new.create(settings, { :nameid => "there" }) + unauth_url = RubySaml::Logoutrequest.new.create(settings, { :nameid => "there" }) inflated = decode_saml_request_payload(unauth_url) assert_match(/ "there" }) + unauth_url = RubySaml::Logoutrequest.new.create(settings, { :nameid => "there" }) inflated = decode_saml_request_payload(unauth_url) assert_match(/([a-zA-Z0-9/+=]+)], inflated @@ -128,7 +128,7 @@ class RequestTest < Minitest::Test end it "sign unsigned request" do - unauth_req = OneLogin::RubySaml::Logoutrequest.new + unauth_req = RubySaml::Logoutrequest.new unauth_req_doc = unauth_req.create_xml_document(settings) inflated = unauth_req_doc.to_s @@ -144,7 +144,7 @@ class RequestTest < Minitest::Test end it "signs through create_logout_request_xml_doc" do - unauth_req = OneLogin::RubySaml::Logoutrequest.new + unauth_req = RubySaml::Logoutrequest.new inflated = unauth_req.create_logout_request_xml_doc(settings).to_s assert_match %r[([a-zA-Z0-9/+=]+)], inflated @@ -155,7 +155,7 @@ class RequestTest < Minitest::Test it "create a signed logout request" do settings.compress_request = true - unauth_req = OneLogin::RubySaml::Logoutrequest.new + unauth_req = RubySaml::Logoutrequest.new unauth_url = unauth_req.create(settings) inflated = decode_saml_request_payload(unauth_url) @@ -167,7 +167,7 @@ class RequestTest < Minitest::Test it "create an uncompressed signed logout request" do settings.compress_request = false - params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings) + params = RubySaml::Logoutrequest.new.create_params(settings) request_xml = Base64.decode64(params["SAMLRequest"]) assert_match %r[([a-zA-Z0-9/+=]+)], request_xml @@ -180,7 +180,7 @@ class RequestTest < Minitest::Test settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA256 settings.security[:digest_method] = XMLSecurity::Document::SHA256 - params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings) + params = RubySaml::Logoutrequest.new.create_params(settings) request_xml = Base64.decode64(params["SAMLRequest"]) assert_match %r[([a-zA-Z0-9/+=]+)], request_xml assert_match %r[], request_xml @@ -192,7 +192,7 @@ class RequestTest < Minitest::Test settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA384 settings.security[:digest_method] = XMLSecurity::Document::SHA512 - params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings) + params = RubySaml::Logoutrequest.new.create_params(settings) request_xml = Base64.decode64(params["SAMLRequest"]) assert_match %r[([a-zA-Z0-9/+=]+)], request_xml @@ -211,7 +211,7 @@ class RequestTest < Minitest::Test ] } - params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings) + params = RubySaml::Logoutrequest.new.create_params(settings) request_xml = Base64.decode64(params["SAMLRequest"]) assert_match %r[([a-zA-Z0-9/+=]+)], request_xml @@ -231,7 +231,7 @@ class RequestTest < Minitest::Test ] } - params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings) + params = RubySaml::Logoutrequest.new.create_params(settings) request_xml = Base64.decode64(params["SAMLRequest"]) assert_match %r[([a-zA-Z0-9/+=]+)], request_xml @@ -242,8 +242,8 @@ class RequestTest < Minitest::Test it "raises error when no valid certs and :check_sp_cert_expiration is true" do settings.security[:check_sp_cert_expiration] = true - assert_raises(OneLogin::RubySaml::ValidationError, 'The SP certificate expired.') do - OneLogin::RubySaml::Logoutrequest.new.create_params(settings) + assert_raises(RubySaml::ValidationError, 'The SP certificate expired.') do + RubySaml::Logoutrequest.new.create_params(settings) end end end @@ -263,7 +263,7 @@ class RequestTest < Minitest::Test it "create a signature parameter with RSA_SHA1 / SHA1 and validate it" do settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA1 - params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') + params = RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') assert params['SAMLRequest'] assert params[:RelayState] assert params['Signature'] @@ -281,7 +281,7 @@ class RequestTest < Minitest::Test it "create a signature parameter with RSA_SHA256 / SHA256 and validate it" do settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA256 - params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') + params = RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') assert params['Signature'] assert_equal params['SigAlg'], XMLSecurity::Document::RSA_SHA256 @@ -297,7 +297,7 @@ class RequestTest < Minitest::Test it "create a signature parameter with RSA_SHA384 / SHA384 and validate it" do settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA384 - params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') + params = RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') assert params['Signature'] assert_equal params['SigAlg'], XMLSecurity::Document::RSA_SHA384 @@ -313,7 +313,7 @@ class RequestTest < Minitest::Test it "create a signature parameter with RSA_SHA512 / SHA512 and validate it" do settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA512 - params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') + params = RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') assert params['Signature'] assert_equal params['SigAlg'], XMLSecurity::Document::RSA_SHA512 @@ -338,7 +338,7 @@ class RequestTest < Minitest::Test ] } - params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') + params = RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') assert params['SAMLRequest'] assert params[:RelayState] assert params['Signature'] @@ -356,8 +356,8 @@ class RequestTest < Minitest::Test it "raises error when no valid certs and :check_sp_cert_expiration is true" do settings.security[:check_sp_cert_expiration] = true - assert_raises(OneLogin::RubySaml::ValidationError, 'The SP certificate expired.') do - OneLogin::RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') + assert_raises(RubySaml::ValidationError, 'The SP certificate expired.') do + RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') end end end @@ -375,7 +375,7 @@ class RequestTest < Minitest::Test it "created a signed logout request" do settings.compress_request = true - unauth_req = OneLogin::RubySaml::Logoutrequest.new + unauth_req = RubySaml::Logoutrequest.new unauth_url = unauth_req.create(settings) inflated = decode_saml_request_payload(unauth_url) @@ -399,7 +399,7 @@ class RequestTest < Minitest::Test it "create a signature parameter with RSA_SHA1 / SHA1 and validate it" do settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA1 - params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') + params = RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') assert params['SAMLRequest'] assert params[:RelayState] assert params['Signature'] @@ -417,7 +417,7 @@ class RequestTest < Minitest::Test describe "#manipulate request_id" do it "be able to modify the request id" do - logoutrequest = OneLogin::RubySaml::Logoutrequest.new + logoutrequest = RubySaml::Logoutrequest.new request_id = logoutrequest.request_id assert_equal request_id, logoutrequest.uuid logoutrequest.uuid = "new_uuid" diff --git a/test/logoutresponse_test.rb b/test/logoutresponse_test.rb index d38541072..cabf58bca 100644 --- a/test/logoutresponse_test.rb +++ b/test/logoutresponse_test.rb @@ -7,12 +7,12 @@ class RubySamlTest < Minitest::Test describe "Logoutresponse" do - let(:valid_logout_response_without_settings) { OneLogin::RubySaml::Logoutresponse.new(valid_logout_response_document) } - let(:valid_logout_response) { OneLogin::RubySaml::Logoutresponse.new(valid_logout_response_document, settings) } + let(:valid_logout_response_without_settings) { RubySaml::Logoutresponse.new(valid_logout_response_document) } + let(:valid_logout_response) { RubySaml::Logoutresponse.new(valid_logout_response_document, settings) } describe "#new" do it "raise an exception when response is initialized with nil" do - assert_raises(ArgumentError) { OneLogin::RubySaml::Logoutresponse.new(nil) } + assert_raises(ArgumentError) { RubySaml::Logoutresponse.new(nil) } end it "default to empty settings" do assert_nil valid_logout_response_without_settings.settings @@ -21,12 +21,12 @@ class RubySamlTest < Minitest::Test refute_nil valid_logout_response.settings end it "accept constructor-injected options" do - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(valid_logout_response_document, nil, { :foo => :bar} ) + logoutresponse = RubySaml::Logoutresponse.new(valid_logout_response_document, nil, { :foo => :bar} ) assert !logoutresponse.options.empty? end it "support base64 encoded responses" do generated_logout_response = valid_logout_response_document - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(Base64.encode64(generated_logout_response), settings) + logoutresponse = RubySaml::Logoutresponse.new(Base64.encode64(generated_logout_response), settings) assert_equal generated_logout_response, logoutresponse.response end end @@ -34,15 +34,15 @@ class RubySamlTest < Minitest::Test describe "#validate_structure" do it "invalidates when the logout response has an invalid xml" do settings.soft = true - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(invalid_xml_logout_response_document, settings) + logoutresponse = RubySaml::Logoutresponse.new(invalid_xml_logout_response_document, settings) assert !logoutresponse.send(:validate_structure) assert_includes logoutresponse.errors, "Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd" end it "raise when the logout response has an invalid xml" do settings.soft = false - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(invalid_xml_logout_response_document, settings) - assert_raises OneLogin::RubySaml::ValidationError do + logoutresponse = RubySaml::Logoutresponse.new(invalid_xml_logout_response_document, settings) + assert_raises RubySaml::ValidationError do logoutresponse.send(:validate_structure) end end @@ -58,7 +58,7 @@ class RubySamlTest < Minitest::Test in_relation_to_request_id = random_id opts = { :matches_request_id => in_relation_to_request_id} - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(valid_logout_response_document({:uuid => in_relation_to_request_id}), settings, opts) + logoutresponse = RubySaml::Logoutresponse.new(valid_logout_response_document({:uuid => in_relation_to_request_id}), settings, opts) assert logoutresponse.validate @@ -74,7 +74,7 @@ class RubySamlTest < Minitest::Test settings.idp_entity_id = 'http://app.muda.no' opts = { :matches_request_id => in_relation_to_request_id} - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(valid_logout_response_document({:uuid => in_relation_to_request_id}), settings, opts) + logoutresponse = RubySaml::Logoutresponse.new(valid_logout_response_document({:uuid => in_relation_to_request_id}), settings, opts) assert logoutresponse.validate assert_equal in_relation_to_request_id, logoutresponse.in_response_to assert logoutresponse.success? @@ -82,7 +82,7 @@ class RubySamlTest < Minitest::Test end it "invalidate logout response when initiated with blank" do - logoutresponse = OneLogin::RubySaml::Logoutresponse.new("", settings) + logoutresponse = RubySaml::Logoutresponse.new("", settings) assert !logoutresponse.validate assert_includes logoutresponse.errors, "Blank logout response" end @@ -91,7 +91,7 @@ class RubySamlTest < Minitest::Test settings.idp_cert_fingerprint = nil settings.idp_cert = nil settings.idp_cert_multi = nil - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(valid_logout_response_document, settings) + logoutresponse = RubySaml::Logoutresponse.new(valid_logout_response_document, settings) assert !logoutresponse.validate assert_includes logoutresponse.errors, "No fingerprint or certificate on settings of the logout response" end @@ -100,7 +100,7 @@ class RubySamlTest < Minitest::Test expected_request_id = "_some_other_expected_uuid" opts = { :matches_request_id => expected_request_id} - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(valid_logout_response_document, settings, opts) + logoutresponse = RubySaml::Logoutresponse.new(valid_logout_response_document, settings, opts) assert !logoutresponse.validate refute_equal expected_request_id, logoutresponse.in_response_to @@ -108,7 +108,7 @@ class RubySamlTest < Minitest::Test end it "invalidate logout response with unexpected request status" do - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(unsuccessful_logout_response_document, settings) + logoutresponse = RubySaml::Logoutresponse.new(unsuccessful_logout_response_document, settings) assert !logoutresponse.success? assert !logoutresponse.validate @@ -116,7 +116,7 @@ class RubySamlTest < Minitest::Test end it "invalidate logout response with unexpected request status and status message" do - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(unsuccessful_logout_response_with_message_document, settings) + logoutresponse = RubySaml::Logoutresponse.new(unsuccessful_logout_response_with_message_document, settings) assert !logoutresponse.success? assert !logoutresponse.validate @@ -127,7 +127,7 @@ class RubySamlTest < Minitest::Test bad_settings = settings bad_settings.issuer = nil bad_settings.sp_entity_id = nil - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(unsuccessful_logout_response_document, bad_settings) + logoutresponse = RubySaml::Logoutresponse.new(unsuccessful_logout_response_document, bad_settings) assert !logoutresponse.validate assert_includes logoutresponse.errors, "No sp_entity_id in settings of the logout response" end @@ -135,14 +135,14 @@ class RubySamlTest < Minitest::Test it "invalidate logout response with wrong issuer" do in_relation_to_request_id = random_id settings.idp_entity_id = 'http://invalid.issuer.example.com/' - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(valid_logout_response_document({:uuid => in_relation_to_request_id}), settings) + logoutresponse = RubySaml::Logoutresponse.new(valid_logout_response_document({:uuid => in_relation_to_request_id}), settings) assert !logoutresponse.validate assert_includes logoutresponse.errors, "Doesn't match the issuer, expected: <#{logoutresponse.settings.idp_entity_id}>, but was: " end it "collect errors when collect_errors=true" do settings.idp_entity_id = 'http://invalid.issuer.example.com/' - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(unsuccessful_logout_response_document, settings) + logoutresponse = RubySaml::Logoutresponse.new(unsuccessful_logout_response_document, settings) collect_errors = true assert !logoutresponse.validate(collect_errors) assert_includes logoutresponse.errors, "The status code of the Logout Response was not Success, was Requester" @@ -159,23 +159,23 @@ class RubySamlTest < Minitest::Test it "validates good logout response" do in_relation_to_request_id = random_id - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(valid_logout_response_document({:uuid => in_relation_to_request_id}), settings) + logoutresponse = RubySaml::Logoutresponse.new(valid_logout_response_document({:uuid => in_relation_to_request_id}), settings) assert logoutresponse.validate assert_empty logoutresponse.errors end it "raises validation error when response initiated with blank" do - logoutresponse = OneLogin::RubySaml::Logoutresponse.new("", settings) + logoutresponse = RubySaml::Logoutresponse.new("", settings) - assert_raises(OneLogin::RubySaml::ValidationError) { logoutresponse.validate } + assert_raises(RubySaml::ValidationError) { logoutresponse.validate } assert_includes logoutresponse.errors, "Blank logout response" end it "raises validation error when initiated with no idp cert or fingerprint" do settings.idp_cert_fingerprint = nil settings.idp_cert = nil - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(valid_logout_response_document, settings) - assert_raises(OneLogin::RubySaml::ValidationError) { logoutresponse.validate } + logoutresponse = RubySaml::Logoutresponse.new(valid_logout_response_document, settings) + assert_raises(RubySaml::ValidationError) { logoutresponse.validate } assert_includes logoutresponse.errors, "No fingerprint or certificate on settings of the logout response" end @@ -184,44 +184,44 @@ class RubySamlTest < Minitest::Test expected_request_id = "_some_other_expected_id" opts = { :matches_request_id => expected_request_id} - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(valid_logout_response_document, settings, opts) - assert_raises(OneLogin::RubySaml::ValidationError) { logoutresponse.validate } + logoutresponse = RubySaml::Logoutresponse.new(valid_logout_response_document, settings, opts) + assert_raises(RubySaml::ValidationError) { logoutresponse.validate } assert_includes logoutresponse.errors, "The InResponseTo of the Logout Response: #{logoutresponse.in_response_to}, does not match the ID of the Logout Request sent by the SP: #{expected_request_id}" end it "raise validation error for wrong request status" do - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(unsuccessful_logout_response_document, settings) + logoutresponse = RubySaml::Logoutresponse.new(unsuccessful_logout_response_document, settings) - assert_raises(OneLogin::RubySaml::ValidationError) { logoutresponse.validate } + assert_raises(RubySaml::ValidationError) { logoutresponse.validate } assert_includes logoutresponse.errors, "The status code of the Logout Response was not Success, was Requester" end it "raise validation error when in bad state" do # no settings - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(unsuccessful_logout_response_document, settings) - assert_raises(OneLogin::RubySaml::ValidationError) { logoutresponse.validate } + logoutresponse = RubySaml::Logoutresponse.new(unsuccessful_logout_response_document, settings) + assert_raises(RubySaml::ValidationError) { logoutresponse.validate } assert_includes logoutresponse.errors, "The status code of the Logout Response was not Success, was Requester" end it "raise validation error when in lack of sp_entity_id setting" do settings.issuer = nil settings.sp_entity_id = nil - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(unsuccessful_logout_response_document, settings) - assert_raises(OneLogin::RubySaml::ValidationError) { logoutresponse.validate } + logoutresponse = RubySaml::Logoutresponse.new(unsuccessful_logout_response_document, settings) + assert_raises(RubySaml::ValidationError) { logoutresponse.validate } assert_includes logoutresponse.errors, "No sp_entity_id in settings of the logout response" end it "raise validation error when logout response with wrong issuer" do in_relation_to_request_id = random_id settings.idp_entity_id = 'http://invalid.issuer.example.com/' - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(valid_logout_response_document({:uuid => in_relation_to_request_id}), settings) - assert_raises(OneLogin::RubySaml::ValidationError) { logoutresponse.validate } + logoutresponse = RubySaml::Logoutresponse.new(valid_logout_response_document({:uuid => in_relation_to_request_id}), settings) + assert_raises(RubySaml::ValidationError) { logoutresponse.validate } assert_includes logoutresponse.errors, "Doesn't match the issuer, expected: <#{logoutresponse.settings.idp_entity_id}>, but was: " end end describe "#validate_signature" do - let (:params) { OneLogin::RubySaml::SloLogoutresponse.new.create_params(settings, random_id, "Custom Logout Message", :RelayState => 'http://example.com') } + let (:params) { RubySaml::SloLogoutresponse.new.create_params(settings, random_id, "Custom Logout Message", :RelayState => 'http://example.com') } before do settings.soft = true @@ -239,7 +239,7 @@ class RubySamlTest < Minitest::Test options = {} options[:get_params] = params options[:relax_signature_validation] = true - logoutresponse_sign_test = OneLogin::RubySaml::Logoutresponse.new(params['SAMLResponse'], settings, options) + logoutresponse_sign_test = RubySaml::Logoutresponse.new(params['SAMLResponse'], settings, options) assert logoutresponse_sign_test.send(:validate_signature) end @@ -249,7 +249,7 @@ class RubySamlTest < Minitest::Test params['RelayState'] = params[:RelayState] options = {} options[:get_params] = params - logoutresponse_sign_test = OneLogin::RubySaml::Logoutresponse.new(params['SAMLResponse'], settings, options) + logoutresponse_sign_test = RubySaml::Logoutresponse.new(params['SAMLResponse'], settings, options) assert !logoutresponse_sign_test.send(:validate_signature) end @@ -258,7 +258,7 @@ class RubySamlTest < Minitest::Test params['RelayState'] = params[:RelayState] options = {} options[:get_params] = params - logoutresponse_sign_test = OneLogin::RubySaml::Logoutresponse.new(params['SAMLResponse'], settings, options) + logoutresponse_sign_test = RubySaml::Logoutresponse.new(params['SAMLResponse'], settings, options) assert logoutresponse_sign_test.send(:validate_signature) end @@ -267,7 +267,7 @@ class RubySamlTest < Minitest::Test params['RelayState'] = params[:RelayState] options = {} options[:get_params] = params - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(params['SAMLResponse'], settings, options) + logoutresponse = RubySaml::Logoutresponse.new(params['SAMLResponse'], settings, options) assert logoutresponse.send(:validate_signature) end @@ -276,7 +276,7 @@ class RubySamlTest < Minitest::Test params['RelayState'] = 'http://invalid.example.com' options = {} options[:get_params] = params - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(params['SAMLResponse'], settings, options) + logoutresponse = RubySaml::Logoutresponse.new(params['SAMLResponse'], settings, options) assert !logoutresponse.send(:validate_signature) end @@ -286,9 +286,9 @@ class RubySamlTest < Minitest::Test params['RelayState'] = 'http://invalid.example.com' options = {} options[:get_params] = params - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(params['SAMLResponse'], settings, options) + logoutresponse = RubySaml::Logoutresponse.new(params['SAMLResponse'], settings, options) - assert_raises(OneLogin::RubySaml::ValidationError) { logoutresponse.send(:validate_signature) } + assert_raises(RubySaml::ValidationError) { logoutresponse.send(:validate_signature) } assert logoutresponse.errors.include? "Invalid Signature on Logout Response" end @@ -298,9 +298,9 @@ class RubySamlTest < Minitest::Test options = {} options[:get_params] = params options[:get_params]['RelayState'] = 'http://example.com' - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(params['SAMLResponse'], settings, options) + logoutresponse = RubySaml::Logoutresponse.new(params['SAMLResponse'], settings, options) # Assemble query string. - query = OneLogin::RubySaml::Utils.build_query( + query = RubySaml::Utils.build_query( :type => 'SAMLResponse', :data => params['SAMLResponse'], :relay_state => params['RelayState'], @@ -322,8 +322,8 @@ class RubySamlTest < Minitest::Test # because it will compute it based on re-encoded query parameters, # rather than their original encodings. options[:get_params] = params - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(params['SAMLResponse'], settings, options) - assert_raises(OneLogin::RubySaml::ValidationError, "Invalid Signature on Logout Request") do + logoutresponse = RubySaml::Logoutresponse.new(params['SAMLResponse'], settings, options) + assert_raises(RubySaml::ValidationError, "Invalid Signature on Logout Request") do logoutresponse.send(:validate_signature) end end @@ -334,9 +334,9 @@ class RubySamlTest < Minitest::Test options = {} options[:get_params] = params options[:get_params]['RelayState'] = 'http://example.com' - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(params['SAMLResponse'], settings, options) + logoutresponse = RubySaml::Logoutresponse.new(params['SAMLResponse'], settings, options) # Assemble query string. - query = OneLogin::RubySaml::Utils.build_query( + query = RubySaml::Utils.build_query( :type => 'SAMLResponse', :data => params['SAMLResponse'], :relay_state => params['RelayState'], @@ -362,13 +362,13 @@ class RubySamlTest < Minitest::Test options[:raw_get_params] = { "RelayState" => "http%3A%2F%2Fex%61mple.com", } - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(params['SAMLResponse'], settings, options) + logoutresponse = RubySaml::Logoutresponse.new(params['SAMLResponse'], settings, options) assert logoutresponse.send(:validate_signature) end end describe "#validate_signature" do - let (:params) { OneLogin::RubySaml::SloLogoutresponse.new.create_params(settings, random_id, "Custom Logout Message", :RelayState => 'http://example.com') } + let (:params) { RubySaml::SloLogoutresponse.new.create_params(settings, random_id, "Custom Logout Message", :RelayState => 'http://example.com') } before do settings.soft = true @@ -388,7 +388,7 @@ class RubySamlTest < Minitest::Test :signing => [ruby_saml_cert_text2, ruby_saml_cert_text], :encryption => [] } - logoutresponse_sign_test = OneLogin::RubySaml::Logoutresponse.new(params['SAMLResponse'], settings, options) + logoutresponse_sign_test = RubySaml::Logoutresponse.new(params['SAMLResponse'], settings, options) assert logoutresponse_sign_test.send(:validate_signature) end @@ -402,7 +402,7 @@ class RubySamlTest < Minitest::Test :signing => [ruby_saml_cert_text], :encryption => [] } - logoutresponse_sign_test = OneLogin::RubySaml::Logoutresponse.new(params['SAMLResponse'], settings, options) + logoutresponse_sign_test = RubySaml::Logoutresponse.new(params['SAMLResponse'], settings, options) assert !logoutresponse_sign_test.send(:validate_signature) assert_includes logoutresponse_sign_test.errors, "IdP x509 certificate expired" end @@ -415,7 +415,7 @@ class RubySamlTest < Minitest::Test :signing => [ruby_saml_cert_text2, ruby_saml_cert_text2], :encryption => [] } - logoutresponse_sign_test = OneLogin::RubySaml::Logoutresponse.new(params['SAMLResponse'], settings, options) + logoutresponse_sign_test = RubySaml::Logoutresponse.new(params['SAMLResponse'], settings, options) assert !logoutresponse_sign_test.send(:validate_signature) assert_includes logoutresponse_sign_test.errors, "Invalid Signature on Logout Response" end diff --git a/test/metadata_test.rb b/test/metadata_test.rb index e09415a53..c69a66e50 100644 --- a/test/metadata_test.rb +++ b/test/metadata_test.rb @@ -5,8 +5,8 @@ class MetadataTest < Minitest::Test describe 'Metadata' do - let(:settings) { OneLogin::RubySaml::Settings.new } - let(:xml_text) { OneLogin::RubySaml::Metadata.new.generate(settings, false) } + let(:settings) { RubySaml::Settings.new } + let(:xml_text) { RubySaml::Metadata.new.generate(settings, false) } let(:xml_doc) { REXML::Document.new(xml_text) } let(:spsso_descriptor) { REXML::XPath.first(xml_doc, "//md:SPSSODescriptor") } let(:acs) { REXML::XPath.first(xml_doc, "//md:AssertionConsumerService") } @@ -18,7 +18,7 @@ class MetadataTest < Minitest::Test end it "generates Pretty Print Service Provider Metadata" do - xml_text = OneLogin::RubySaml::Metadata.new.generate(settings, true) + xml_text = RubySaml::Metadata.new.generate(settings, true) # assert correct xml declaration start = "\n "there" }) + auth_url = RubySaml::Authrequest.new.create(settings, { :hello => "there" }) assert_match(/&hello=there$/, auth_url) - auth_url = OneLogin::RubySaml::Authrequest.new.create(settings, { :hello => nil }) + auth_url = RubySaml::Authrequest.new.create(settings, { :hello => nil }) assert_match(/&hello=$/, auth_url) end it "RelayState cases" do - auth_url = OneLogin::RubySaml::Authrequest.new.create(settings, { :RelayState => nil }) + auth_url = RubySaml::Authrequest.new.create(settings, { :RelayState => nil }) assert !auth_url.include?('RelayState') - auth_url = OneLogin::RubySaml::Authrequest.new.create(settings, { :RelayState => "http://example.com" }) + auth_url = RubySaml::Authrequest.new.create(settings, { :RelayState => "http://example.com" }) assert auth_url.include?('&RelayState=http%3A%2F%2Fexample.com') - auth_url = OneLogin::RubySaml::Authrequest.new.create(settings, { 'RelayState' => nil }) + auth_url = RubySaml::Authrequest.new.create(settings, { 'RelayState' => nil }) assert !auth_url.include?('RelayState') - auth_url = OneLogin::RubySaml::Authrequest.new.create(settings, { 'RelayState' => "http://example.com" }) + auth_url = RubySaml::Authrequest.new.create(settings, { 'RelayState' => "http://example.com" }) assert auth_url.include?('&RelayState=http%3A%2F%2Fexample.com') end it "creates request with ID prefixed with default '_'" do - request = OneLogin::RubySaml::Authrequest.new + request = RubySaml::Authrequest.new assert_match(/^_/, request.uuid) end it "creates request with ID is prefixed, when :id_prefix is passed" do - OneLogin::RubySaml::Utils::set_prefix("test") - request = OneLogin::RubySaml::Authrequest.new + RubySaml::Utils::set_prefix("test") + request = RubySaml::Authrequest.new assert_match(/^test/, request.uuid) - OneLogin::RubySaml::Utils::set_prefix("_") + RubySaml::Utils::set_prefix("_") end describe "when the target url is not set" do @@ -182,8 +182,8 @@ class RequestTest < Minitest::Test end it "raises an error with a descriptive message" do - err = assert_raises OneLogin::RubySaml::SettingError do - OneLogin::RubySaml::Authrequest.new.create(settings) + err = assert_raises RubySaml::SettingError do + RubySaml::Authrequest.new.create(settings) end assert_match(/idp_sso_service_url is not set/, err.message) end @@ -192,7 +192,7 @@ class RequestTest < Minitest::Test describe "when the target url doesn't contain a query string" do it "create the SAMLRequest parameter correctly" do - auth_url = OneLogin::RubySaml::Authrequest.new.create(settings) + auth_url = RubySaml::Authrequest.new.create(settings) assert_match(/^http:\/\/example.com\?SAMLRequest/, auth_url) end end @@ -201,27 +201,27 @@ class RequestTest < Minitest::Test it "create the SAMLRequest parameter correctly" do settings.idp_sso_service_url = "http://example.com?field=value" - auth_url = OneLogin::RubySaml::Authrequest.new.create(settings) + auth_url = RubySaml::Authrequest.new.create(settings) assert_match(/^http:\/\/example.com\?field=value&SAMLRequest/, auth_url) end end it "create the saml:AuthnContextClassRef element correctly" do settings.authn_context = 'secure/name/password/uri' - auth_doc = OneLogin::RubySaml::Authrequest.new.create_authentication_xml_doc(settings) + auth_doc = RubySaml::Authrequest.new.create_authentication_xml_doc(settings) assert_match(/secure\/name\/password\/uri<\/saml:AuthnContextClassRef>/, auth_doc.to_s) end it "create multiple saml:AuthnContextClassRef elements correctly" do settings.authn_context = ['secure/name/password/uri', 'secure/email/password/uri'] - auth_doc = OneLogin::RubySaml::Authrequest.new.create_authentication_xml_doc(settings) + auth_doc = RubySaml::Authrequest.new.create_authentication_xml_doc(settings) assert_match(/secure\/name\/password\/uri<\/saml:AuthnContextClassRef>/, auth_doc.to_s) assert_match(/secure\/email\/password\/uri<\/saml:AuthnContextClassRef>/, auth_doc.to_s) end it "create the saml:AuthnContextClassRef with comparison exact" do settings.authn_context = 'secure/name/password/uri' - auth_doc = OneLogin::RubySaml::Authrequest.new.create_authentication_xml_doc(settings) + auth_doc = RubySaml::Authrequest.new.create_authentication_xml_doc(settings) assert_match(/secure\/name\/password\/uri<\/saml:AuthnContextClassRef>/, auth_doc.to_s) end @@ -229,14 +229,14 @@ class RequestTest < Minitest::Test it "create the saml:AuthnContextClassRef with comparison minimun" do settings.authn_context = 'secure/name/password/uri' settings.authn_context_comparison = 'minimun' - auth_doc = OneLogin::RubySaml::Authrequest.new.create_authentication_xml_doc(settings) + auth_doc = RubySaml::Authrequest.new.create_authentication_xml_doc(settings) assert_match(/secure\/name\/password\/uri<\/saml:AuthnContextClassRef>/, auth_doc.to_s) end it "create the saml:AuthnContextDeclRef element correctly" do settings.authn_context_decl_ref = 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport' - auth_doc = OneLogin::RubySaml::Authrequest.new.create_authentication_xml_doc(settings) + auth_doc = RubySaml::Authrequest.new.create_authentication_xml_doc(settings) assert_match(/urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport<\/saml:AuthnContextDeclRef>/, auth_doc.to_s) end @@ -251,7 +251,7 @@ class RequestTest < Minitest::Test end it "create a signed request" do - params = OneLogin::RubySaml::Authrequest.new.create_params(settings) + params = RubySaml::Authrequest.new.create_params(settings) request_xml = Base64.decode64(params["SAMLRequest"]) assert_match %r[([a-zA-Z0-9/+=]+)], request_xml assert_match %r[], request_xml @@ -261,7 +261,7 @@ class RequestTest < Minitest::Test settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA256 settings.security[:digest_method] = XMLSecurity::Document::SHA512 - params = OneLogin::RubySaml::Authrequest.new.create_params(settings) + params = RubySaml::Authrequest.new.create_params(settings) request_xml = Base64.decode64(params["SAMLRequest"]) assert_match %r[([a-zA-Z0-9/+=]+)], request_xml @@ -279,7 +279,7 @@ class RequestTest < Minitest::Test ] } - params = OneLogin::RubySaml::Authrequest.new.create_params(settings) + params = RubySaml::Authrequest.new.create_params(settings) request_xml = Base64.decode64(params["SAMLRequest"]) assert_match %r[([a-zA-Z0-9/+=]+)], request_xml @@ -297,7 +297,7 @@ class RequestTest < Minitest::Test ] } - params = OneLogin::RubySaml::Authrequest.new.create_params(settings) + params = RubySaml::Authrequest.new.create_params(settings) request_xml = Base64.decode64(params["SAMLRequest"]) assert_match %r[([a-zA-Z0-9/+=]+)], request_xml @@ -307,8 +307,8 @@ class RequestTest < Minitest::Test it "raises error when no valid certs and :check_sp_cert_expiration is true" do settings.security[:check_sp_cert_expiration] = true - assert_raises(OneLogin::RubySaml::ValidationError, 'The SP certificate expired.') do - OneLogin::RubySaml::Authrequest.new.create_params(settings) + assert_raises(RubySaml::ValidationError, 'The SP certificate expired.') do + RubySaml::Authrequest.new.create_params(settings) end end end @@ -329,7 +329,7 @@ class RequestTest < Minitest::Test it "create a signature parameter with RSA_SHA1 and validate it" do settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA1 - params = OneLogin::RubySaml::Authrequest.new.create_params(settings, :RelayState => 'http://example.com') + params = RubySaml::Authrequest.new.create_params(settings, :RelayState => 'http://example.com') assert params['SAMLRequest'] assert params[:RelayState] assert params['Signature'] @@ -347,7 +347,7 @@ class RequestTest < Minitest::Test it "create a signature parameter with RSA_SHA256 and validate it" do settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA256 - params = OneLogin::RubySaml::Authrequest.new.create_params(settings, :RelayState => 'http://example.com') + params = RubySaml::Authrequest.new.create_params(settings, :RelayState => 'http://example.com') assert params['Signature'] assert_equal params['SigAlg'], XMLSecurity::Document::RSA_SHA256 @@ -372,7 +372,7 @@ class RequestTest < Minitest::Test ] } - params = OneLogin::RubySaml::Authrequest.new.create_params(settings, :RelayState => 'http://example.com') + params = RubySaml::Authrequest.new.create_params(settings, :RelayState => 'http://example.com') assert params['SAMLRequest'] assert params[:RelayState] assert params['Signature'] @@ -390,21 +390,21 @@ class RequestTest < Minitest::Test it "raises error when no valid certs and :check_sp_cert_expiration is true" do settings.security[:check_sp_cert_expiration] = true - assert_raises(OneLogin::RubySaml::ValidationError, 'The SP certificate expired.') do - OneLogin::RubySaml::Authrequest.new.create_params(settings, :RelayState => 'http://example.com') + assert_raises(RubySaml::ValidationError, 'The SP certificate expired.') do + RubySaml::Authrequest.new.create_params(settings, :RelayState => 'http://example.com') end end end it "create the saml:AuthnContextClassRef element correctly" do settings.authn_context = 'secure/name/password/uri' - auth_doc = OneLogin::RubySaml::Authrequest.new.create_authentication_xml_doc(settings) + auth_doc = RubySaml::Authrequest.new.create_authentication_xml_doc(settings) assert auth_doc.to_s =~ /secure\/name\/password\/uri<\/saml:AuthnContextClassRef>/ end it "create the saml:AuthnContextClassRef with comparison exact" do settings.authn_context = 'secure/name/password/uri' - auth_doc = OneLogin::RubySaml::Authrequest.new.create_authentication_xml_doc(settings) + auth_doc = RubySaml::Authrequest.new.create_authentication_xml_doc(settings) assert auth_doc.to_s =~ /secure\/name\/password\/uri<\/saml:AuthnContextClassRef>/ end @@ -412,20 +412,20 @@ class RequestTest < Minitest::Test it "create the saml:AuthnContextClassRef with comparison minimun" do settings.authn_context = 'secure/name/password/uri' settings.authn_context_comparison = 'minimun' - auth_doc = OneLogin::RubySaml::Authrequest.new.create_authentication_xml_doc(settings) + auth_doc = RubySaml::Authrequest.new.create_authentication_xml_doc(settings) assert auth_doc.to_s =~ /secure\/name\/password\/uri<\/saml:AuthnContextClassRef>/ end it "create the saml:AuthnContextDeclRef element correctly" do settings.authn_context_decl_ref = 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport' - auth_doc = OneLogin::RubySaml::Authrequest.new.create_authentication_xml_doc(settings) + auth_doc = RubySaml::Authrequest.new.create_authentication_xml_doc(settings) assert auth_doc.to_s =~ /urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport<\/saml:AuthnContextDeclRef>/ end it "create multiple saml:AuthnContextDeclRef elements correctly " do settings.authn_context_decl_ref = ['name/password/uri', 'example/decl/ref'] - auth_doc = OneLogin::RubySaml::Authrequest.new.create_authentication_xml_doc(settings) + auth_doc = RubySaml::Authrequest.new.create_authentication_xml_doc(settings) assert auth_doc.to_s =~ /name\/password\/uri<\/saml:AuthnContextDeclRef>/ assert auth_doc.to_s =~ /example\/decl\/ref<\/saml:AuthnContextDeclRef>/ end @@ -441,7 +441,7 @@ class RequestTest < Minitest::Test end it "create a signed request" do - params = OneLogin::RubySaml::Authrequest.new.create_params(settings) + params = RubySaml::Authrequest.new.create_params(settings) request_xml = Base64.decode64(params["SAMLRequest"]) assert_match %r[([a-zA-Z0-9/+=]+)], request_xml assert_match %r[], request_xml @@ -464,7 +464,7 @@ class RequestTest < Minitest::Test it "create a signature parameter with RSA_SHA1 and validate it" do settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA1 - params = OneLogin::RubySaml::Authrequest.new.create_params(settings, :RelayState => 'http://example.com') + params = RubySaml::Authrequest.new.create_params(settings, :RelayState => 'http://example.com') assert params['SAMLRequest'] assert params[:RelayState] assert params['Signature'] @@ -483,7 +483,7 @@ class RequestTest < Minitest::Test describe "#manipulate request_id" do it "be able to modify the request id" do - authnrequest = OneLogin::RubySaml::Authrequest.new + authnrequest = RubySaml::Authrequest.new request_id = authnrequest.request_id assert_equal request_id, authnrequest.uuid authnrequest.uuid = "new_uuid" diff --git a/test/response_test.rb b/test/response_test.rb index 4a7d0874c..7b80a75b1 100644 --- a/test/response_test.rb +++ b/test/response_test.rb @@ -6,56 +6,56 @@ class RubySamlTest < Minitest::Test describe "Response" do - let(:settings) { OneLogin::RubySaml::Settings.new } - let(:response) { OneLogin::RubySaml::Response.new(response_document_without_recipient) } - let(:response_without_attributes) { OneLogin::RubySaml::Response.new(response_document_without_attributes) } - let(:response_with_multiple_attribute_statements) { OneLogin::RubySaml::Response.new(fixture(:response_with_multiple_attribute_statements)) } - let(:response_without_reference_uri) { OneLogin::RubySaml::Response.new(response_document_without_reference_uri) } - let(:response_with_signed_assertion) { OneLogin::RubySaml::Response.new(response_document_with_signed_assertion) } - let(:response_with_ds_namespace_at_the_root) { OneLogin::RubySaml::Response.new(response_document_with_ds_namespace_at_the_root)} - let(:response_unsigned) { OneLogin::RubySaml::Response.new(response_document_unsigned) } - let(:response_wrapped) { OneLogin::RubySaml::Response.new(response_document_wrapped) } - let(:response_multiple_attr_values) { OneLogin::RubySaml::Response.new(fixture(:response_with_multiple_attribute_values)) } - let(:response_valid_signed) { OneLogin::RubySaml::Response.new(response_document_valid_signed) } - let(:response_valid_signed_without_recipient) { OneLogin::RubySaml::Response.new(response_document_valid_signed, {:skip_recipient_check => true })} - let(:response_valid_signed_without_x509certificate) { OneLogin::RubySaml::Response.new(response_document_valid_signed_without_x509certificate) } - let(:response_no_id) { OneLogin::RubySaml::Response.new(read_invalid_response("no_id.xml.base64")) } - let(:response_no_version) { OneLogin::RubySaml::Response.new(read_invalid_response("no_saml2.xml.base64")) } - let(:response_multi_assertion) { OneLogin::RubySaml::Response.new(read_invalid_response("multiple_assertions.xml.base64")) } - let(:response_no_conditions) { OneLogin::RubySaml::Response.new(read_invalid_response("no_conditions.xml.base64")) } - let(:response_no_conditions_with_skip) { OneLogin::RubySaml::Response.new(read_invalid_response("no_conditions.xml.base64"), { :skip_conditions => true }) } - let(:response_no_authnstatement) { OneLogin::RubySaml::Response.new(read_invalid_response("no_authnstatement.xml.base64")) } - let(:response_no_authnstatement_with_skip) { OneLogin::RubySaml::Response.new(read_invalid_response("no_authnstatement.xml.base64"), {:skip_authnstatement => true}) } - let(:response_empty_destination) { OneLogin::RubySaml::Response.new(read_invalid_response("empty_destination.xml.base64")) } - let(:response_empty_destination_with_skip) { OneLogin::RubySaml::Response.new(read_invalid_response("empty_destination.xml.base64"), {:skip_destination => true}) } - let(:response_no_status) { OneLogin::RubySaml::Response.new(read_invalid_response("no_status.xml.base64")) } - let(:response_no_statuscode) { OneLogin::RubySaml::Response.new(read_invalid_response("no_status_code.xml.base64")) } - let(:response_statuscode_responder) { OneLogin::RubySaml::Response.new(read_invalid_response("status_code_responder.xml.base64")) } - let(:response_statuscode_responder_and_msg) { OneLogin::RubySaml::Response.new(read_invalid_response("status_code_responer_and_msg.xml.base64")) } - let(:response_double_statuscode) { OneLogin::RubySaml::Response.new(response_document_double_status_code) } - let(:response_encrypted_attrs) { OneLogin::RubySaml::Response.new(response_document_encrypted_attrs) } - let(:response_no_signed_elements) { OneLogin::RubySaml::Response.new(read_invalid_response("no_signature.xml.base64")) } - let(:response_multiple_signed) { OneLogin::RubySaml::Response.new(read_invalid_response("multiple_signed.xml.base64")) } - let(:response_audience_self_closed) { OneLogin::RubySaml::Response.new(read_response("response_audience_self_closed_tag.xml.base64")) } - let(:response_invalid_audience) { OneLogin::RubySaml::Response.new(read_invalid_response("invalid_audience.xml.base64")) } - let(:response_invalid_audience_with_skip) { OneLogin::RubySaml::Response.new(read_invalid_response("invalid_audience.xml.base64"), {:skip_audience => true}) } - let(:response_invalid_signed_element) { OneLogin::RubySaml::Response.new(read_invalid_response("response_invalid_signed_element.xml.base64")) } - let(:response_invalid_issuer_assertion) { OneLogin::RubySaml::Response.new(read_invalid_response("invalid_issuer_assertion.xml.base64")) } - let(:response_invalid_issuer_message) { OneLogin::RubySaml::Response.new(read_invalid_response("invalid_issuer_message.xml.base64")) } - let(:response_no_issuer_response) { OneLogin::RubySaml::Response.new(read_invalid_response("no_issuer_response.xml.base64")) } - let(:response_no_issuer_assertion) { OneLogin::RubySaml::Response.new(read_invalid_response("no_issuer_assertion.xml.base64")) } - let(:response_no_nameid) { OneLogin::RubySaml::Response.new(read_invalid_response("no_nameid.xml.base64")) } - let(:response_empty_nameid) { OneLogin::RubySaml::Response.new(read_invalid_response("empty_nameid.xml.base64")) } - let(:response_wrong_spnamequalifier) { OneLogin::RubySaml::Response.new(read_invalid_response("wrong_spnamequalifier.xml.base64")) } - let(:response_duplicated_attributes) { OneLogin::RubySaml::Response.new(read_invalid_response("duplicated_attributes.xml.base64")) } - let(:response_no_subjectconfirmation_data) { OneLogin::RubySaml::Response.new(read_invalid_response("no_subjectconfirmation_data.xml.base64")) } - let(:response_no_subjectconfirmation_method) { OneLogin::RubySaml::Response.new(read_invalid_response("no_subjectconfirmation_method.xml.base64")) } - let(:response_invalid_subjectconfirmation_inresponse) { OneLogin::RubySaml::Response.new(read_invalid_response("invalid_subjectconfirmation_inresponse.xml.base64")) } - let(:response_invalid_subjectconfirmation_recipient) { OneLogin::RubySaml::Response.new(read_invalid_response("invalid_subjectconfirmation_recipient.xml.base64")) } - let(:response_invalid_subjectconfirmation_nb) { OneLogin::RubySaml::Response.new(read_invalid_response("invalid_subjectconfirmation_nb.xml.base64")) } - let(:response_invalid_subjectconfirmation_noa) { OneLogin::RubySaml::Response.new(read_invalid_response("invalid_subjectconfirmation_noa.xml.base64")) } - let(:response_invalid_signature_position) { OneLogin::RubySaml::Response.new(read_invalid_response("invalid_signature_position.xml.base64")) } - let(:response_encrypted_nameid) { OneLogin::RubySaml::Response.new(response_document_encrypted_nameid) } + let(:settings) { RubySaml::Settings.new } + let(:response) { RubySaml::Response.new(response_document_without_recipient) } + let(:response_without_attributes) { RubySaml::Response.new(response_document_without_attributes) } + let(:response_with_multiple_attribute_statements) { RubySaml::Response.new(fixture(:response_with_multiple_attribute_statements)) } + let(:response_without_reference_uri) { RubySaml::Response.new(response_document_without_reference_uri) } + let(:response_with_signed_assertion) { RubySaml::Response.new(response_document_with_signed_assertion) } + let(:response_with_ds_namespace_at_the_root) { RubySaml::Response.new(response_document_with_ds_namespace_at_the_root)} + let(:response_unsigned) { RubySaml::Response.new(response_document_unsigned) } + let(:response_wrapped) { RubySaml::Response.new(response_document_wrapped) } + let(:response_multiple_attr_values) { RubySaml::Response.new(fixture(:response_with_multiple_attribute_values)) } + let(:response_valid_signed) { RubySaml::Response.new(response_document_valid_signed) } + let(:response_valid_signed_without_recipient) { RubySaml::Response.new(response_document_valid_signed, {:skip_recipient_check => true })} + let(:response_valid_signed_without_x509certificate) { RubySaml::Response.new(response_document_valid_signed_without_x509certificate) } + let(:response_no_id) { RubySaml::Response.new(read_invalid_response("no_id.xml.base64")) } + let(:response_no_version) { RubySaml::Response.new(read_invalid_response("no_saml2.xml.base64")) } + let(:response_multi_assertion) { RubySaml::Response.new(read_invalid_response("multiple_assertions.xml.base64")) } + let(:response_no_conditions) { RubySaml::Response.new(read_invalid_response("no_conditions.xml.base64")) } + let(:response_no_conditions_with_skip) { RubySaml::Response.new(read_invalid_response("no_conditions.xml.base64"), { :skip_conditions => true }) } + let(:response_no_authnstatement) { RubySaml::Response.new(read_invalid_response("no_authnstatement.xml.base64")) } + let(:response_no_authnstatement_with_skip) { RubySaml::Response.new(read_invalid_response("no_authnstatement.xml.base64"), {:skip_authnstatement => true}) } + let(:response_empty_destination) { RubySaml::Response.new(read_invalid_response("empty_destination.xml.base64")) } + let(:response_empty_destination_with_skip) { RubySaml::Response.new(read_invalid_response("empty_destination.xml.base64"), {:skip_destination => true}) } + let(:response_no_status) { RubySaml::Response.new(read_invalid_response("no_status.xml.base64")) } + let(:response_no_statuscode) { RubySaml::Response.new(read_invalid_response("no_status_code.xml.base64")) } + let(:response_statuscode_responder) { RubySaml::Response.new(read_invalid_response("status_code_responder.xml.base64")) } + let(:response_statuscode_responder_and_msg) { RubySaml::Response.new(read_invalid_response("status_code_responer_and_msg.xml.base64")) } + let(:response_double_statuscode) { RubySaml::Response.new(response_document_double_status_code) } + let(:response_encrypted_attrs) { RubySaml::Response.new(response_document_encrypted_attrs) } + let(:response_no_signed_elements) { RubySaml::Response.new(read_invalid_response("no_signature.xml.base64")) } + let(:response_multiple_signed) { RubySaml::Response.new(read_invalid_response("multiple_signed.xml.base64")) } + let(:response_audience_self_closed) { RubySaml::Response.new(read_response("response_audience_self_closed_tag.xml.base64")) } + let(:response_invalid_audience) { RubySaml::Response.new(read_invalid_response("invalid_audience.xml.base64")) } + let(:response_invalid_audience_with_skip) { RubySaml::Response.new(read_invalid_response("invalid_audience.xml.base64"), {:skip_audience => true}) } + let(:response_invalid_signed_element) { RubySaml::Response.new(read_invalid_response("response_invalid_signed_element.xml.base64")) } + let(:response_invalid_issuer_assertion) { RubySaml::Response.new(read_invalid_response("invalid_issuer_assertion.xml.base64")) } + let(:response_invalid_issuer_message) { RubySaml::Response.new(read_invalid_response("invalid_issuer_message.xml.base64")) } + let(:response_no_issuer_response) { RubySaml::Response.new(read_invalid_response("no_issuer_response.xml.base64")) } + let(:response_no_issuer_assertion) { RubySaml::Response.new(read_invalid_response("no_issuer_assertion.xml.base64")) } + let(:response_no_nameid) { RubySaml::Response.new(read_invalid_response("no_nameid.xml.base64")) } + let(:response_empty_nameid) { RubySaml::Response.new(read_invalid_response("empty_nameid.xml.base64")) } + let(:response_wrong_spnamequalifier) { RubySaml::Response.new(read_invalid_response("wrong_spnamequalifier.xml.base64")) } + let(:response_duplicated_attributes) { RubySaml::Response.new(read_invalid_response("duplicated_attributes.xml.base64")) } + let(:response_no_subjectconfirmation_data) { RubySaml::Response.new(read_invalid_response("no_subjectconfirmation_data.xml.base64")) } + let(:response_no_subjectconfirmation_method) { RubySaml::Response.new(read_invalid_response("no_subjectconfirmation_method.xml.base64")) } + let(:response_invalid_subjectconfirmation_inresponse) { RubySaml::Response.new(read_invalid_response("invalid_subjectconfirmation_inresponse.xml.base64")) } + let(:response_invalid_subjectconfirmation_recipient) { RubySaml::Response.new(read_invalid_response("invalid_subjectconfirmation_recipient.xml.base64")) } + let(:response_invalid_subjectconfirmation_nb) { RubySaml::Response.new(read_invalid_response("invalid_subjectconfirmation_nb.xml.base64")) } + let(:response_invalid_subjectconfirmation_noa) { RubySaml::Response.new(read_invalid_response("invalid_subjectconfirmation_noa.xml.base64")) } + let(:response_invalid_signature_position) { RubySaml::Response.new(read_invalid_response("invalid_signature_position.xml.base64")) } + let(:response_encrypted_nameid) { RubySaml::Response.new(response_document_encrypted_nameid) } def generate_audience_error(expected, actual) s = actual.count > 1 ? 's' : ''; @@ -63,21 +63,21 @@ def generate_audience_error(expected, actual) end it "raise an exception when response is initialized with nil" do - assert_raises(ArgumentError) { OneLogin::RubySaml::Response.new(nil) } + assert_raises(ArgumentError) { RubySaml::Response.new(nil) } end it "not filter available options only" do options = { :skip_destination => true, :foo => :bar } - response = OneLogin::RubySaml::Response.new(response_document_valid_signed, options) + response = RubySaml::Response.new(response_document_valid_signed, options) assert_includes response.options.keys, :skip_destination assert_includes response.options.keys, :foo end it "be able to parse a document which contains ampersands" do XMLSecurity::SignedDocument.any_instance.stubs(:digests_match?).returns(true) - OneLogin::RubySaml::Response.any_instance.stubs(:validate_conditions).returns(true) + RubySaml::Response.any_instance.stubs(:validate_conditions).returns(true) - ampersands_response = OneLogin::RubySaml::Response.new(ampersands_document) + ampersands_response = RubySaml::Response.new(ampersands_document) ampersands_response.settings = settings ampersands_response.settings.idp_cert_fingerprint = 'c51985d947f1be57082025050846eb27f6cab783' @@ -87,7 +87,7 @@ def generate_audience_error(expected, actual) describe "Prevent node text with comment attack (VU#475445)" do before do - @response = OneLogin::RubySaml::Response.new(read_response('response_node_text_attack.xml.base64')) + @response = RubySaml::Response.new(read_response('response_node_text_attack.xml.base64')) end it "receives the full NameID when there is an injected comment" do @@ -101,7 +101,7 @@ def generate_audience_error(expected, actual) describe "Another test to prevent with comment attack (VU#475445)" do before do - @response = OneLogin::RubySaml::Response.new(read_response('response_node_text_attack2.xml.base64'), {:skip_recipient_check => true }) + @response = RubySaml::Response.new(read_response('response_node_text_attack2.xml.base64'), {:skip_recipient_check => true }) @response.settings = settings @response.settings.idp_cert_fingerprint = ruby_saml_cert_fingerprint end @@ -113,7 +113,7 @@ def generate_audience_error(expected, actual) describe "Another test with CDATA injected" do before do - @response = OneLogin::RubySaml::Response.new(read_response('response_node_text_attack3.xml.base64'), {:skip_recipient_check => true }) + @response = RubySaml::Response.new(read_response('response_node_text_attack3.xml.base64'), {:skip_recipient_check => true }) @response.settings = settings @response.settings.idp_cert_fingerprint = ruby_saml_cert_fingerprint end @@ -127,7 +127,7 @@ def generate_audience_error(expected, actual) describe "Prevent XEE attack" do before do - @response = OneLogin::RubySaml::Response.new(fixture(:attackxee)) + @response = RubySaml::Response.new(fixture(:attackxee)) end it "false when evil attack vector is present, soft = true" do @@ -139,7 +139,7 @@ def generate_audience_error(expected, actual) it "raise when evil attack vector is present, soft = false " do @response.soft = false - assert_raises(OneLogin::RubySaml::ValidationError) do + assert_raises(RubySaml::ValidationError) do @response.send(:validate_structure) end end @@ -153,7 +153,7 @@ def generate_audience_error(expected, actual) it "default to raw input when a response is not Base64 encoded" do decoded = Base64.decode64(response_document_without_attributes) - response_from_raw = OneLogin::RubySaml::Response.new(decoded) + response_from_raw = RubySaml::Response.new(decoded) assert response_from_raw.document end @@ -175,10 +175,10 @@ def generate_audience_error(expected, actual) end it "raise when response is initialized with blank data" do - blank_response = OneLogin::RubySaml::Response.new('') + blank_response = RubySaml::Response.new('') blank_response.soft = false error_msg = "Blank response" - assert_raises(OneLogin::RubySaml::ValidationError, error_msg) do + assert_raises(RubySaml::ValidationError, error_msg) do blank_response.is_valid? end assert_includes blank_response.errors, error_msg @@ -186,7 +186,7 @@ def generate_audience_error(expected, actual) it "raise when settings have not been set" do error_msg = "No settings on response" - assert_raises(OneLogin::RubySaml::ValidationError, error_msg) do + assert_raises(RubySaml::ValidationError, error_msg) do response.is_valid? end assert_includes response.errors, error_msg @@ -198,7 +198,7 @@ def generate_audience_error(expected, actual) settings.idp_cert_multi = nil response.settings = settings error_msg = "No fingerprint or certificate on settings" - assert_raises(OneLogin::RubySaml::ValidationError, error_msg) do + assert_raises(RubySaml::ValidationError, error_msg) do response.is_valid? end assert_includes response.errors, error_msg @@ -217,7 +217,7 @@ def generate_audience_error(expected, actual) response_no_signed_elements.settings = settings response_no_signed_elements.soft = false error_msg = "Found an unexpected number of Signature Element. SAML Response rejected" - assert_raises(OneLogin::RubySaml::ValidationError, error_msg) do + assert_raises(RubySaml::ValidationError, error_msg) do response_no_signed_elements.is_valid? end end @@ -227,19 +227,19 @@ def generate_audience_error(expected, actual) response_multiple_signed.settings = settings response_multiple_signed.soft = false error_msg = "Duplicated ID. SAML Response rejected" - assert_raises(OneLogin::RubySaml::ValidationError, error_msg) do + assert_raises(RubySaml::ValidationError, error_msg) do response_multiple_signed.is_valid? end end it "validate SAML 2.0 XML structure" do resp_xml = Base64.decode64(response_document_unsigned).gsub(/emailAddress/,'test') - response_unsigned_mod = OneLogin::RubySaml::Response.new(Base64.encode64(resp_xml)) + response_unsigned_mod = RubySaml::Response.new(Base64.encode64(resp_xml)) response_unsigned_mod.stubs(:conditions).returns(nil) settings.idp_cert_fingerprint = signature_fingerprint_1 response_unsigned_mod.settings = settings response_unsigned_mod.soft = false - assert_raises(OneLogin::RubySaml::ValidationError, 'Digest mismatch') do + assert_raises(RubySaml::ValidationError, 'Digest mismatch') do response_unsigned_mod.is_valid? end end @@ -249,7 +249,7 @@ def generate_audience_error(expected, actual) response.settings = settings response.soft = false error_msg = "Current time is on or after NotOnOrAfter condition" - assert_raises(OneLogin::RubySaml::ValidationError, error_msg) do + assert_raises(RubySaml::ValidationError, error_msg) do response.is_valid? end assert_includes response.errors[0], error_msg @@ -259,7 +259,7 @@ def generate_audience_error(expected, actual) settings.idp_cert_fingerprint = signature_fingerprint_1 response_without_attributes.settings = settings response_without_attributes.soft = false - assert_raises(OneLogin::RubySaml::ValidationError) do + assert_raises(RubySaml::ValidationError) do response_without_attributes.is_valid? end end @@ -270,9 +270,9 @@ def generate_audience_error(expected, actual) opts = {} opts[:settings] = settings opts[:matches_request_id] = "invalid_request_id" - response_valid_signed = OneLogin::RubySaml::Response.new(response_document_valid_signed, opts) + response_valid_signed = RubySaml::Response.new(response_document_valid_signed, opts) error_msg = "The InResponseTo of the Response: _fc4a34b0-7efb-012e-caae-782bcb13bb38, does not match the ID of the AuthNRequest sent by the SP: invalid_request_id" - assert_raises(OneLogin::RubySaml::ValidationError, error_msg) do + assert_raises(RubySaml::ValidationError, error_msg) do response_valid_signed.is_valid? end assert_includes response_valid_signed.errors, error_msg @@ -284,7 +284,7 @@ def generate_audience_error(expected, actual) response_valid_signed.settings = settings response_valid_signed.soft = false error_msg = generate_audience_error(response_valid_signed.settings.sp_entity_id, ['https://someone.example.com/audience']) - assert_raises(OneLogin::RubySaml::ValidationError, error_msg) do + assert_raises(RubySaml::ValidationError, error_msg) do response_valid_signed.is_valid? end assert_includes response_valid_signed.errors, error_msg @@ -295,7 +295,7 @@ def generate_audience_error(expected, actual) response_no_id.settings = settings response_no_id.soft = false error_msg = "Missing ID attribute on SAML Response" - assert_raises(OneLogin::RubySaml::ValidationError, error_msg) do + assert_raises(RubySaml::ValidationError, error_msg) do response_no_id.is_valid? end assert_includes response_no_id.errors, error_msg @@ -306,7 +306,7 @@ def generate_audience_error(expected, actual) response_no_version.settings = settings response_no_version.soft = false error_msg = "Unsupported SAML version" - assert_raises(OneLogin::RubySaml::ValidationError, error_msg) do + assert_raises(RubySaml::ValidationError, error_msg) do response_no_version.is_valid? end assert_includes response_no_version.errors, error_msg @@ -336,7 +336,7 @@ def generate_audience_error(expected, actual) end it "return false when response is initialized with blank data" do - blank_response = OneLogin::RubySaml::Response.new('') + blank_response = RubySaml::Response.new('') blank_response.soft = true assert !blank_response.is_valid? assert_includes blank_response.errors, "Blank response" @@ -377,7 +377,7 @@ def generate_audience_error(expected, actual) end it "support dynamic namespace resolution on signature elements" do - no_signature_response = OneLogin::RubySaml::Response.new(fixture("no_signature_ns.xml")) + no_signature_response = RubySaml::Response.new(fixture("no_signature_ns.xml")) no_signature_response.stubs(:conditions).returns(nil) no_signature_response.stubs(:validate_subject_confirmation).returns(true) no_signature_response.settings = settings @@ -387,7 +387,7 @@ def generate_audience_error(expected, actual) end it "validate ADFS assertions" do - adfs_response = OneLogin::RubySaml::Response.new(fixture(:adfs_response_sha256)) + adfs_response = RubySaml::Response.new(fixture(:adfs_response_sha256)) adfs_response.stubs(:conditions).returns(nil) adfs_response.stubs(:validate_subject_confirmation).returns(true) settings.idp_cert_fingerprint = "28:74:9B:E8:1F:E8:10:9C:A8:7C:A9:C3:E3:C5:01:6C:92:1C:B4:BA" @@ -398,7 +398,7 @@ def generate_audience_error(expected, actual) it "validate SAML 2.0 XML structure" do resp_xml = Base64.decode64(response_document_unsigned).gsub(/emailAddress/,'test') - response_unsigned_mod = OneLogin::RubySaml::Response.new(Base64.encode64(resp_xml)) + response_unsigned_mod = RubySaml::Response.new(Base64.encode64(resp_xml)) response_unsigned_mod.stubs(:conditions).returns(nil) settings.idp_cert_fingerprint = signature_fingerprint_1 response_unsigned_mod.settings = settings @@ -429,7 +429,7 @@ def generate_audience_error(expected, actual) opts = {} opts[:settings] = settings opts[:matches_request_id] = "invalid_request_id" - response_valid_signed = OneLogin::RubySaml::Response.new(response_document_valid_signed, opts) + response_valid_signed = RubySaml::Response.new(response_document_valid_signed, opts) response_valid_signed.is_valid? assert_includes response_valid_signed.errors, "The InResponseTo of the Response: _fc4a34b0-7efb-012e-caae-782bcb13bb38, does not match the ID of the AuthNRequest sent by the SP: invalid_request_id" end @@ -650,7 +650,7 @@ def generate_audience_error(expected, actual) it "raise when encountering a mailformed element that prevents the document from being valid" do response_without_attributes.soft = false - assert_raises(OneLogin::RubySaml::ValidationError) { + assert_raises(RubySaml::ValidationError) { response_without_attributes.send(:validate_structure) } end @@ -658,7 +658,7 @@ def generate_audience_error(expected, actual) describe "validate_formatted_x509_certificate" do let(:response_with_formatted_x509certificate) { - OneLogin::RubySaml::Response.new(read_response("valid_response_with_formatted_x509certificate.xml.base64"), { + RubySaml::Response.new(read_response("valid_response_with_formatted_x509certificate.xml.base64"), { :skip_conditions => true, :skip_subject_confirmation => true }) } @@ -673,19 +673,19 @@ def generate_audience_error(expected, actual) describe "#validate_in_response_to" do it "return true when the inResponseTo value matches the Request ID" do - response = OneLogin::RubySaml::Response.new(response_document_valid_signed, :settings => settings, :matches_request_id => "_fc4a34b0-7efb-012e-caae-782bcb13bb38") + response = RubySaml::Response.new(response_document_valid_signed, :settings => settings, :matches_request_id => "_fc4a34b0-7efb-012e-caae-782bcb13bb38") assert response.send(:validate_in_response_to) assert_empty response.errors end it "return true when no Request ID is provided for checking" do - response = OneLogin::RubySaml::Response.new(response_document_valid_signed, :settings => settings) + response = RubySaml::Response.new(response_document_valid_signed, :settings => settings) assert response.send(:validate_in_response_to) assert_empty response.errors end it "return false when the inResponseTo value does not match the Request ID" do - response = OneLogin::RubySaml::Response.new(response_document_valid_signed, :settings => settings, :matches_request_id => "invalid_request_id") + response = RubySaml::Response.new(response_document_valid_signed, :settings => settings, :matches_request_id => "invalid_request_id") assert !response.send(:validate_in_response_to) assert_includes response.errors, "The InResponseTo of the Response: _fc4a34b0-7efb-012e-caae-782bcb13bb38, does not match the ID of the AuthNRequest sent by the SP: invalid_request_id" end @@ -824,7 +824,7 @@ def generate_audience_error(expected, actual) it "return true when the skip_subject_confirmation option is passed and the subject confirmation is valid" do opts = {} opts[:skip_subject_confirmation] = true - response_with_skip = OneLogin::RubySaml::Response.new(response_document_valid_signed, opts) + response_with_skip = RubySaml::Response.new(response_document_valid_signed, opts) response_with_skip.settings = settings response_with_skip.settings.assertion_consumer_service_url = 'recipient' Time.expects(:now).times(0) # ensures the test isn't run and thus Time.now.utc is never called within the test @@ -835,7 +835,7 @@ def generate_audience_error(expected, actual) it "return true when the skip_subject_confirmation option is passed and the response has an invalid subject confirmation" do opts = {} opts[:skip_subject_confirmation] = true - response_with_skip = OneLogin::RubySaml::Response.new(read_invalid_response("invalid_subjectconfirmation_noa.xml.base64"), opts) + response_with_skip = RubySaml::Response.new(read_invalid_response("invalid_subjectconfirmation_noa.xml.base64"), opts) response_with_skip.settings = settings Time.expects(:now).times(0) # ensures the test isn't run and thus Time.now.utc is never called within the test assert response_with_skip.send(:validate_subject_confirmation) @@ -862,7 +862,7 @@ def generate_audience_error(expected, actual) opts = {} opts[:allowed_clock_drift] = drift - response_with_drift = OneLogin::RubySaml::Response.new(response_document_without_recipient, opts) + response_with_drift = RubySaml::Response.new(response_document_without_recipient, opts) response_with_drift.settings = settings assert response_with_drift.send(:validate_session_expiration) assert_empty response_with_drift.errors @@ -887,7 +887,7 @@ def generate_audience_error(expected, actual) it "return true when the signature is valid and fingerprint provided" do settings.idp_cert_fingerprint = '49:EC:3F:A4:71:8A:1E:C9:DB:70:A7:CC:33:36:96:F0:48:8C:4E:DA' xml = 'PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIERlc3RpbmF0aW9uPSJodHRwczovL2NvZGVycGFkLmlvL3NhbWwvYWNzIiBJRD0iXzEwOGE1ZTg0MDllYzRjZjlhY2QxYzQ2OWU5ZDcxNGFkIiBJblJlc3BvbnNlVG89Il80ZmZmYWE2MC02OTZiLTAxMzMtMzg4Ni0wMjQxZjY1YzA2OTMiIElzc3VlSW5zdGFudD0iMjAxNS0xMS0wOVQyMzo1NTo0M1oiIFZlcnNpb249IjIuMCI+PHNhbWw6SXNzdWVyIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iPmh0dHBzOi8vbG9naW4uaHVsdS5jb208L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIj48L2RzOkNhbm9uaWNhbGl6YXRpb25NZXRob2Q+PGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSI+PC9kczpTaWduYXR1cmVNZXRob2Q+PGRzOlJlZmVyZW5jZSBVUkk9IiNfMTA4YTVlODQwOWVjNGNmOWFjZDFjNDY5ZTlkNzE0YWQiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSI+PC9kczpUcmFuc2Zvcm0+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyI+PC9kczpUcmFuc2Zvcm0+PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSI+PC9kczpEaWdlc3RNZXRob2Q+PGRzOkRpZ2VzdFZhbHVlPm9sQllXbTQyRi9oZm0xdHJYTHk2a3V6MXlMUT08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+dXNRTmY5WGpKTDRlOXVucnVCdWViSnQ3R0tXM2hJUk9teWVqTm1NMHM4WFhlWHN3WHc4U3ZCZi8zeDNNWEpkWnpNV0pOM3ExN2tGWHN2bTVna1JzbkE9PTwvZHM6U2lnbmF0dXJlVmFsdWU+PGRzOktleUluZm8+PGRzOlg1MDlEYXRhPjxkczpYNTA5Q2VydGlmaWNhdGU+TUlJQ1FEQ0NBZXFnQXdJQkFnSUpBSVZOdzVLRzR1aTFNQTBHQ1NxR1NJYjNEUUVCQlFVQU1Fd3hDekFKQmdOVkJBWVRBa2RDTVJJd0VBWURWUVFJRXdsQ1pYSnJjMmhwY21VeEVEQU9CZ05WQkFjVEIwNWxkMkoxY25reEZ6QVZCZ05WQkFvVERrMTVJRU52YlhCaGJua2dUSFJrTUI0WERURXlNVEF5TlRBMk1qY3pORm9YRFRJeU1UQXlNekEyTWpjek5Gb3dUREVMTUFrR0ExVUVCaE1DUjBJeEVqQVFCZ05WQkFnVENVSmxjbXR6YUdseVpURVFNQTRHQTFVRUJ4TUhUbVYzWW5WeWVURVhNQlVHQTFVRUNoTU9UWGtnUTI5dGNHRnVlU0JNZEdRd1hEQU5CZ2txaGtpRzl3MEJBUUVGQUFOTEFEQklBa0VBd1NOL2dpMzNSbXBBUW9MUWo3UDZ6QW5OVDBSbjdiakMzMjNuM3ExT25mdm52UjBmUWp2TnQ3ckRrQTVBdjVRbk02VjRZVU5Vbk1mYk9RcTBXTGJMU3dJREFRQUJvNEd1TUlHck1CMEdBMVVkRGdRV0JCUWZJSDFvZkJWcHNSQWNJTUsyaGJsN25nTVRZREI4QmdOVkhTTUVkVEJ6Z0JRZklIMW9mQlZwc1JBY0lNSzJoYmw3bmdNVFlLRlFwRTR3VERFTE1Ba0dBMVVFQmhNQ1IwSXhFakFRQmdOVkJBZ1RDVUpsY210emFHbHlaVEVRTUE0R0ExVUVCeE1IVG1WM1luVnllVEVYTUJVR0ExVUVDaE1PVFhrZ1EyOXRjR0Z1ZVNCTWRHU0NDUUNGVGNPU2h1TG90VEFNQmdOVkhSTUVCVEFEQVFIL01BMEdDU3FHU0liM0RRRUJCUVVBQTBFQXFvZ1YzdVBjbEtYRG1EWk1UN3ZsUFl4TEFxQ0dIWnRsQ3h6NGhNNEtTdGxEMi9HTmMxWGlMYjFoL0swQ0pMRG9zckVJYm0zd2lPMk12VEVSclZZU01RPT08L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT48c2FtbHA6U3RhdHVzPjxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiPjwvc2FtbHA6U3RhdHVzQ29kZT48L3NhbWxwOlN0YXR1cz48c2FtbDpBc3NlcnRpb24geG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9Il8wMTg4MmRhOTM2OTQ0ZDFlYTZlZmY0NDA2NTc2MzFiNSIgSXNzdWVJbnN0YW50PSIyMDE1LTExLTA5VDIzOjU1OjQzWiIgVmVyc2lvbj0iMi4wIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9sb2dpbi5odWx1LmNvbTwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+PGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiPjwvZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZD48ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIj48L2RzOlNpZ25hdHVyZU1ldGhvZD48ZHM6UmVmZXJlbmNlIFVSST0iI18wMTg4MmRhOTM2OTQ0ZDFlYTZlZmY0NDA2NTc2MzFiNSI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIj48L2RzOlRyYW5zZm9ybT48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIj48L2RzOlRyYW5zZm9ybT48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIj48L2RzOkRpZ2VzdE1ldGhvZD48ZHM6RGlnZXN0VmFsdWU+cmo2YzhucC9BUmV0ZkJ1dWVOSzNPS0xDYnowPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5hR05FemZHM1dLcExKc2ZLRGJSNmpva2d6OEFnZ0FIRVVESEZyd0dsTHVQeWpyNEl3M09NcFNkV2gyL01YK1F3M1dPTk5mNHJNalh5TGVZSFJIVGpMQT09PC9kczpTaWduYXR1cmVWYWx1ZT48ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDUURDQ0FlcWdBd0lCQWdJSkFJVk53NUtHNHVpMU1BMEdDU3FHU0liM0RRRUJCUVVBTUV3eEN6QUpCZ05WQkFZVEFrZENNUkl3RUFZRFZRUUlFd2xDWlhKcmMyaHBjbVV4RURBT0JnTlZCQWNUQjA1bGQySjFjbmt4RnpBVkJnTlZCQW9URGsxNUlFTnZiWEJoYm5rZ1RIUmtNQjRYRFRFeU1UQXlOVEEyTWpjek5Gb1hEVEl5TVRBeU16QTJNamN6TkZvd1RERUxNQWtHQTFVRUJoTUNSMEl4RWpBUUJnTlZCQWdUQ1VKbGNtdHphR2x5WlRFUU1BNEdBMVVFQnhNSFRtVjNZblZ5ZVRFWE1CVUdBMVVFQ2hNT1RYa2dRMjl0Y0dGdWVTQk1kR1F3WERBTkJna3Foa2lHOXcwQkFRRUZBQU5MQURCSUFrRUF3U04vZ2kzM1JtcEFRb0xRajdQNnpBbk5UMFJuN2JqQzMyM24zcTFPbmZ2bnZSMGZRanZOdDdyRGtBNUF2NVFuTTZWNFlVTlVuTWZiT1FxMFdMYkxTd0lEQVFBQm80R3VNSUdyTUIwR0ExVWREZ1FXQkJRZklIMW9mQlZwc1JBY0lNSzJoYmw3bmdNVFlEQjhCZ05WSFNNRWRUQnpnQlFmSUgxb2ZCVnBzUkFjSU1LMmhibDduZ01UWUtGUXBFNHdUREVMTUFrR0ExVUVCaE1DUjBJeEVqQVFCZ05WQkFnVENVSmxjbXR6YUdseVpURVFNQTRHQTFVRUJ4TUhUbVYzWW5WeWVURVhNQlVHQTFVRUNoTU9UWGtnUTI5dGNHRnVlU0JNZEdTQ0NRQ0ZUY09TaHVMb3RUQU1CZ05WSFJNRUJUQURBUUgvTUEwR0NTcUdTSWIzRFFFQkJRVUFBMEVBcW9nVjN1UGNsS1hEbURaTVQ3dmxQWXhMQXFDR0hadGxDeHo0aE00S1N0bEQyL0dOYzFYaUxiMWgvSzBDSkxEb3NyRUlibTN3aU8yTXZURVJyVllTTVE9PTwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIiBTUE5hbWVRdWFsaWZpZXI9Imh0dHBzOi8vY29kZXJwYWQuaW8iPm1hdHQuanVyaWtAaHVsdS5jb208L3NhbWw6TmFtZUlEPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBJblJlc3BvbnNlVG89Il80ZmZmYWE2MC02OTZiLTAxMzMtMzg4Ni0wMjQxZjY1YzA2OTMiIE5vdE9uT3JBZnRlcj0iMjAxNS0xMS0xMFQwMDoxMDo0M1oiIFJlY2lwaWVudD0iaHR0cHM6Ly9jb2RlcnBhZC5pby9zYW1sL2FjcyI+PC9zYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE1LTExLTA5VDIyOjU1OjQzWiIgTm90T25PckFmdGVyPSIyMDE1LTExLTEwVDAwOjEwOjQzWiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5odHRwczovL2NvZGVycGFkLmlvPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNS0xMS0wOVQyMzo1NTo0M1oiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9IkdpdmVuLW5hbWUiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlPk1hdHQ8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iU3VybmFtZSI+PHNhbWw6QXR0cmlidXRlVmFsdWU+SnVyaWs8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iRW1haWwiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlPm1hdHQuanVyaWtAaHVsdS5jb208L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48L3NhbWw6QXR0cmlidXRlU3RhdGVtZW50Pjwvc2FtbDpBc3NlcnRpb24+PC9zYW1scDpSZXNwb25zZT4=' - response_x = OneLogin::RubySaml::Response.new(xml) + response_x = RubySaml::Response.new(xml) response_x.settings = settings assert response_x.send(:validate_signature) assert_empty response_x.errors @@ -941,7 +941,7 @@ def generate_audience_error(expected, actual) content = read_response('response_with_signed_message_and_assertion.xml') content = content.sub(/.*<\/ds:X509Certificate>/, "an-invalid-certificate") - response_invalid_x509certificate = OneLogin::RubySaml::Response.new(content) + response_invalid_x509certificate = RubySaml::Response.new(content) response_invalid_x509certificate.settings = settings assert !response_invalid_x509certificate.send(:validate_signature) assert_includes response_invalid_x509certificate.errors, "Document Certificate Error" @@ -958,7 +958,7 @@ def generate_audience_error(expected, actual) it "return false when signature wrapping attack" do signature_wrapping_attack = read_invalid_response("signature_wrapping_attack.xml.base64") - response_wrapped = OneLogin::RubySaml::Response.new(signature_wrapping_attack) + response_wrapped = RubySaml::Response.new(signature_wrapping_attack) response_wrapped.stubs(:conditions).returns(nil) response_wrapped.stubs(:validate_subject_confirmation).returns(true) settings.idp_cert_fingerprint = "afe71c28ef740bc87425be13a2263d37971da1f9" @@ -1050,12 +1050,12 @@ def generate_audience_error(expected, actual) end it "be extractable from an OpenSAML response" do - response_open_saml = OneLogin::RubySaml::Response.new(fixture(:open_saml)) + response_open_saml = RubySaml::Response.new(fixture(:open_saml)) assert_equal "someone@example.org", response_open_saml.nameid end it "be extractable from a Simple SAML PHP response" do - response_ssp = OneLogin::RubySaml::Response.new(fixture(:simple_saml_php)) + response_ssp = RubySaml::Response.new(fixture(:simple_saml_php)) assert_equal "someone@example.com", response_ssp.nameid end end @@ -1069,7 +1069,7 @@ def generate_audience_error(expected, actual) describe "#sessionindex" do it "extract the value of the sessionindex element" do - response = OneLogin::RubySaml::Response.new(fixture(:simple_saml_php)) + response = RubySaml::Response.new(fixture(:simple_saml_php)) assert_equal "_51be37965feb5579d803141076936dc2e9d1d98ebf", response.sessionindex end end @@ -1115,11 +1115,11 @@ def generate_audience_error(expected, actual) it "check time conditions" do response.soft = true assert !response.send(:validate_conditions) - response_time_updated = OneLogin::RubySaml::Response.new(response_document_without_recipient_with_time_updated) + response_time_updated = RubySaml::Response.new(response_document_without_recipient_with_time_updated) response_time_updated.soft = true assert response_time_updated.send(:validate_conditions) Timecop.freeze(Time.parse("2011-06-14T18:25:01.516Z")) do - response_with_saml2_namespace = OneLogin::RubySaml::Response.new(response_document_with_saml2_namespace) + response_with_saml2_namespace = RubySaml::Response.new(response_document_with_saml2_namespace) response_with_saml2_namespace.soft = true assert response_with_saml2_namespace.send(:validate_conditions) end @@ -1130,27 +1130,27 @@ def generate_audience_error(expected, actual) # The NotBefore condition in the document is 2011-06-14T18:21:01.516Z Timecop.freeze(Time.parse("2011-06-14T18:21:01Z")) do - special_response_with_saml2_namespace = OneLogin::RubySaml::Response.new( + special_response_with_saml2_namespace = RubySaml::Response.new( response_document_with_saml2_namespace, :allowed_clock_drift => 0.515, :settings => settings ) assert !special_response_with_saml2_namespace.send(:validate_conditions) - special_response_with_saml2_namespace = OneLogin::RubySaml::Response.new( + special_response_with_saml2_namespace = RubySaml::Response.new( response_document_with_saml2_namespace, :allowed_clock_drift => 0.516 ) assert special_response_with_saml2_namespace.send(:validate_conditions) - special_response_with_saml2_namespace = OneLogin::RubySaml::Response.new( + special_response_with_saml2_namespace = RubySaml::Response.new( response_document_with_saml2_namespace, :allowed_clock_drift => '0.515', :settings => settings ) assert !special_response_with_saml2_namespace.send(:validate_conditions) - special_response_with_saml2_namespace = OneLogin::RubySaml::Response.new( + special_response_with_saml2_namespace = RubySaml::Response.new( response_document_with_saml2_namespace, :allowed_clock_drift => '0.516' ) @@ -1166,27 +1166,27 @@ def generate_audience_error(expected, actual) # The NotBefore condition in the document is 2011-06-1418:31:01.516Z Timecop.freeze(Time.parse("2011-06-14T18:31:02Z")) do - special_response_with_saml2_namespace = OneLogin::RubySaml::Response.new( + special_response_with_saml2_namespace = RubySaml::Response.new( response_document_with_saml2_namespace, :allowed_clock_drift => 0.483, :settings => settings ) assert !special_response_with_saml2_namespace.send(:validate_conditions) - special_response_with_saml2_namespace = OneLogin::RubySaml::Response.new( + special_response_with_saml2_namespace = RubySaml::Response.new( response_document_with_saml2_namespace, :allowed_clock_drift => java ? 0.485 : 0.484 ) assert special_response_with_saml2_namespace.send(:validate_conditions) - special_response_with_saml2_namespace = OneLogin::RubySaml::Response.new( + special_response_with_saml2_namespace = RubySaml::Response.new( response_document_with_saml2_namespace, :allowed_clock_drift => '0.483', :settings => settings ) assert !special_response_with_saml2_namespace.send(:validate_conditions) - special_response_with_saml2_namespace = OneLogin::RubySaml::Response.new( + special_response_with_saml2_namespace = RubySaml::Response.new( response_document_with_saml2_namespace, :allowed_clock_drift => java ? '0.485' : '0.484' ) @@ -1219,14 +1219,14 @@ def generate_audience_error(expected, actual) end it "not raise on responses without attributes" do - assert_equal OneLogin::RubySaml::Attributes.new, response_unsigned.attributes + assert_equal RubySaml::Attributes.new, response_unsigned.attributes end describe "#encrypted attributes" do it "raise error when the assertion contains encrypted attributes but no private key to decrypt" do settings.private_key = nil response_encrypted_attrs.settings = settings - assert_raises(OneLogin::RubySaml::ValidationError, "An EncryptedAttribute found and no SP private key found on the settings to decrypt it") do + assert_raises(RubySaml::ValidationError, "An EncryptedAttribute found and no SP private key found on the settings to decrypt it") do response_encrypted_attrs.attributes end end @@ -1259,10 +1259,10 @@ def generate_audience_error(expected, actual) end it "extract single value as string in compatibility mode off" do - OneLogin::RubySaml::Attributes.single_value_compatibility = false + RubySaml::Attributes.single_value_compatibility = false assert_equal ["demo"], response_multiple_attr_values.attributes[:uid] # classes are not reloaded between tests so restore default - OneLogin::RubySaml::Attributes.single_value_compatibility = true + RubySaml::Attributes.single_value_compatibility = true end it "extract first of multiple values as string for b/w compatibility" do @@ -1270,9 +1270,9 @@ def generate_audience_error(expected, actual) end it "extract first of multiple values as string for b/w compatibility in compatibility mode off" do - OneLogin::RubySaml::Attributes.single_value_compatibility = false + RubySaml::Attributes.single_value_compatibility = false assert_equal ['value1', 'value2'], response_multiple_attr_values.attributes[:another_value] - OneLogin::RubySaml::Attributes.single_value_compatibility = true + RubySaml::Attributes.single_value_compatibility = true end it "return array with all attributes when asked in XML order" do @@ -1280,9 +1280,9 @@ def generate_audience_error(expected, actual) end it "return array with all attributes when asked in XML order in compatibility mode off" do - OneLogin::RubySaml::Attributes.single_value_compatibility = false + RubySaml::Attributes.single_value_compatibility = false assert_equal ['value1', 'value2'], response_multiple_attr_values.attributes.multi(:another_value) - OneLogin::RubySaml::Attributes.single_value_compatibility = true + RubySaml::Attributes.single_value_compatibility = true end it "return first of multiple values when multiple Attribute tags in XML" do @@ -1290,9 +1290,9 @@ def generate_audience_error(expected, actual) end it "return first of multiple values when multiple Attribute tags in XML in compatibility mode off" do - OneLogin::RubySaml::Attributes.single_value_compatibility = false + RubySaml::Attributes.single_value_compatibility = false assert_equal ['role1', 'role2', 'role3'], response_multiple_attr_values.attributes[:role] - OneLogin::RubySaml::Attributes.single_value_compatibility = true + RubySaml::Attributes.single_value_compatibility = true end it "return all of multiple values in reverse order when multiple Attribute tags in XML" do @@ -1300,15 +1300,15 @@ def generate_audience_error(expected, actual) end it "return all of multiple values in reverse order when multiple Attribute tags in XML in compatibility mode off" do - OneLogin::RubySaml::Attributes.single_value_compatibility = false + RubySaml::Attributes.single_value_compatibility = false assert_equal ['role1', 'role2', 'role3'], response_multiple_attr_values.attributes.multi(:role) - OneLogin::RubySaml::Attributes.single_value_compatibility = true + RubySaml::Attributes.single_value_compatibility = true end it "return all of multiple values when multiple Attribute tags in multiple AttributeStatement tags" do - OneLogin::RubySaml::Attributes.single_value_compatibility = false + RubySaml::Attributes.single_value_compatibility = false assert_equal ['role1', 'role2', 'role3'], response_with_multiple_attribute_statements.attributes.multi(:role) - OneLogin::RubySaml::Attributes.single_value_compatibility = true + RubySaml::Attributes.single_value_compatibility = true end it "return nil value correctly" do @@ -1316,20 +1316,20 @@ def generate_audience_error(expected, actual) end it "return nil value correctly when not in compatibility mode off" do - OneLogin::RubySaml::Attributes.single_value_compatibility = false + RubySaml::Attributes.single_value_compatibility = false assert_equal [nil], response_multiple_attr_values.attributes[:attribute_with_nil_value] - OneLogin::RubySaml::Attributes.single_value_compatibility = true + RubySaml::Attributes.single_value_compatibility = true end it "return multiple values including nil and empty string" do - response = OneLogin::RubySaml::Response.new(fixture(:response_with_multiple_attribute_values)) + response = RubySaml::Response.new(fixture(:response_with_multiple_attribute_values)) assert_equal ["", "valuePresent", nil, nil], response.attributes.multi(:attribute_with_nils_and_empty_strings) end it "return multiple values from [] when not in compatibility mode off" do - OneLogin::RubySaml::Attributes.single_value_compatibility = false + RubySaml::Attributes.single_value_compatibility = false assert_equal ["", "valuePresent", nil, nil], response_multiple_attr_values.attributes[:attribute_with_nils_and_empty_strings] - OneLogin::RubySaml::Attributes.single_value_compatibility = true + RubySaml::Attributes.single_value_compatibility = true end it "check what happens when trying retrieve attribute that does not exists" do @@ -1337,11 +1337,11 @@ def generate_audience_error(expected, actual) assert_nil response_multiple_attr_values.attributes.single(:attribute_not_exists) assert_nil response_multiple_attr_values.attributes.multi(:attribute_not_exists) - OneLogin::RubySaml::Attributes.single_value_compatibility = false + RubySaml::Attributes.single_value_compatibility = false assert_nil response_multiple_attr_values.attributes[:attribute_not_exists] assert_nil response_multiple_attr_values.attributes.single(:attribute_not_exists) assert_nil response_multiple_attr_values.attributes.multi(:attribute_not_exists) - OneLogin::RubySaml::Attributes.single_value_compatibility = true + RubySaml::Attributes.single_value_compatibility = true end end @@ -1367,7 +1367,7 @@ def generate_audience_error(expected, actual) it 'not allow arbitrary code execution' do $evalled = nil malicious_response_document = fixture('response_eval', false) - malicious_response = OneLogin::RubySaml::Response.new(malicious_response_document) + malicious_response = RubySaml::Response.new(malicious_response_document) malicious_response.send(:xpath_first_from_signed_assertion) assert_nil $evalled end @@ -1379,14 +1379,14 @@ def generate_audience_error(expected, actual) document = XMLSecurity::Document.new(xml) - formatted_cert = OneLogin::RubySaml::Utils.format_cert(ruby_saml_cert_text) + formatted_cert = RubySaml::Utils.format_cert(ruby_saml_cert_text) cert = OpenSSL::X509::Certificate.new(formatted_cert) - formatted_private_key = OneLogin::RubySaml::Utils.format_private_key(ruby_saml_key_text) + formatted_private_key = RubySaml::Utils.format_private_key(ruby_saml_key_text) private_key = OpenSSL::PKey::RSA.new(formatted_private_key) document.sign_document(private_key, cert) - signed_response = OneLogin::RubySaml::Response.new(document.to_s) + signed_response = RubySaml::Response.new(document.to_s) settings.assertion_consumer_service_url = "http://recipient" settings.idp_cert = ruby_saml_cert_text signed_response.settings = settings @@ -1400,8 +1400,8 @@ def generate_audience_error(expected, actual) describe '#want_assertion_signed' do before do settings.security[:want_assertions_signed] = true - @signed_assertion = OneLogin::RubySaml::Response.new(response_document_with_signed_assertion, :settings => settings) - @no_signed_assertion = OneLogin::RubySaml::Response.new(response_document_valid_signed, :settings => settings) + @signed_assertion = RubySaml::Response.new(response_document_with_signed_assertion, :settings => settings) + @no_signed_assertion = RubySaml::Response.new(response_document_valid_signed, :settings => settings) end it 'returns false if :want_assertion_signed enabled and Assertion not signed' do @@ -1423,11 +1423,11 @@ def generate_audience_error(expected, actual) it 'is not possible when encryptID inside the assertion but no private key' do response_encrypted_nameid.settings = settings - assert_raises(OneLogin::RubySaml::ValidationError, "An EncryptedID found and no SP private key found on the settings to decrypt it") do + assert_raises(RubySaml::ValidationError, "An EncryptedID found and no SP private key found on the settings to decrypt it") do assert_equal "test@onelogin.com", response_encrypted_nameid.nameid end - assert_raises(OneLogin::RubySaml::ValidationError, "An EncryptedID found and no SP private key found on the settings to decrypt it") do + assert_raises(RubySaml::ValidationError, "An EncryptedID found and no SP private key found on the settings to decrypt it") do assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", response_encrypted_nameid.name_id_format end end @@ -1445,18 +1445,18 @@ def generate_audience_error(expected, actual) it 'raise if an encrypted assertion is found and no sp private key to decrypt it' do error_msg = "An EncryptedAssertion found and no SP private key found on the settings to decrypt it. Be sure you provided the :settings parameter at the initialize method" - assert_raises(OneLogin::RubySaml::ValidationError, error_msg) do - OneLogin::RubySaml::Response.new(signed_message_encrypted_unsigned_assertion) + assert_raises(RubySaml::ValidationError, error_msg) do + RubySaml::Response.new(signed_message_encrypted_unsigned_assertion) end - assert_raises(OneLogin::RubySaml::ValidationError, error_msg) do - OneLogin::RubySaml::Response.new(signed_message_encrypted_unsigned_assertion, :settings => settings) + assert_raises(RubySaml::ValidationError, error_msg) do + RubySaml::Response.new(signed_message_encrypted_unsigned_assertion, :settings => settings) end settings.certificate = ruby_saml_cert_text settings.private_key = ruby_saml_key_text - assert_raises(OneLogin::RubySaml::ValidationError, error_msg) do - response3 = OneLogin::RubySaml::Response.new(signed_message_encrypted_unsigned_assertion) + assert_raises(RubySaml::ValidationError, error_msg) do + response3 = RubySaml::Response.new(signed_message_encrypted_unsigned_assertion) response3.settings end end @@ -1468,26 +1468,26 @@ def generate_audience_error(expected, actual) error_msg = "Neither PUB key nor PRIV key: nested asn1 error" assert_raises(OpenSSL::PKey::RSAError, error_msg) do - OneLogin::RubySaml::Response.new(signed_message_encrypted_unsigned_assertion, :settings => settings) + RubySaml::Response.new(signed_message_encrypted_unsigned_assertion, :settings => settings) end end it 'return true if an encrypted assertion is found and settings initialized with private_key' do settings.certificate = ruby_saml_cert_text settings.private_key = ruby_saml_key_text - response = OneLogin::RubySaml::Response.new(signed_message_encrypted_unsigned_assertion, :settings => settings) + response = RubySaml::Response.new(signed_message_encrypted_unsigned_assertion, :settings => settings) assert response.decrypted_document - response2 = OneLogin::RubySaml::Response.new(signed_message_encrypted_signed_assertion, :settings => settings) + response2 = RubySaml::Response.new(signed_message_encrypted_signed_assertion, :settings => settings) assert response2.decrypted_document - response3 = OneLogin::RubySaml::Response.new(unsigned_message_encrypted_signed_assertion, :settings => settings) + response3 = RubySaml::Response.new(unsigned_message_encrypted_signed_assertion, :settings => settings) assert response3.decrypted_document - response4 = OneLogin::RubySaml::Response.new(unsigned_message_encrypted_unsigned_assertion, :settings => settings) + response4 = RubySaml::Response.new(unsigned_message_encrypted_unsigned_assertion, :settings => settings) assert response4.decrypted_document - assert OneLogin::RubySaml::Response.new( + assert RubySaml::Response.new( Base64.encode64(File.read('test/responses/unsigned_encrypted_adfs.xml')), :settings => settings ).decrypted_document @@ -1505,7 +1505,7 @@ def generate_audience_error(expected, actual) end it 'is possible when signed_message_encrypted_unsigned_assertion' do - response = OneLogin::RubySaml::Response.new(signed_message_encrypted_unsigned_assertion, :settings => settings) + response = RubySaml::Response.new(signed_message_encrypted_unsigned_assertion, :settings => settings) Timecop.freeze(Time.parse("2015-03-19T14:30:31Z")) do assert response.is_valid? assert_empty response.errors @@ -1515,7 +1515,7 @@ def generate_audience_error(expected, actual) end it 'is possible when signed_message_encrypted_signed_assertion' do - response = OneLogin::RubySaml::Response.new(signed_message_encrypted_signed_assertion, :settings => settings) + response = RubySaml::Response.new(signed_message_encrypted_signed_assertion, :settings => settings) Timecop.freeze(Time.parse("2015-03-19T14:30:31Z")) do assert response.is_valid? assert_empty response.errors @@ -1525,7 +1525,7 @@ def generate_audience_error(expected, actual) end it 'is possible when unsigned_message_encrypted_signed_assertion' do - response = OneLogin::RubySaml::Response.new(unsigned_message_encrypted_signed_assertion, :settings => settings) + response = RubySaml::Response.new(unsigned_message_encrypted_signed_assertion, :settings => settings) Timecop.freeze(Time.parse("2015-03-19T14:30:31Z")) do assert response.is_valid? assert_empty response.errors @@ -1535,7 +1535,7 @@ def generate_audience_error(expected, actual) end it 'is not possible when unsigned_message_encrypted_unsigned_assertion' do - response = OneLogin::RubySaml::Response.new(unsigned_message_encrypted_unsigned_assertion, :settings => settings) + response = RubySaml::Response.new(unsigned_message_encrypted_unsigned_assertion, :settings => settings) Timecop.freeze(Time.parse("2015-03-19T14:30:31Z")) do assert !response.is_valid? assert_includes response.errors, "Found an unexpected number of Signature Element. SAML Response rejected" @@ -1551,7 +1551,7 @@ def generate_audience_error(expected, actual) describe "check right settings" do it "is not possible to decrypt the assertion if no private key" do - response = OneLogin::RubySaml::Response.new(signed_message_encrypted_unsigned_assertion, :settings => settings) + response = RubySaml::Response.new(signed_message_encrypted_unsigned_assertion, :settings => settings) encrypted_assertion_node = REXML::XPath.first( response.document, @@ -1561,7 +1561,7 @@ def generate_audience_error(expected, actual) response.settings.private_key = nil error_msg = "An EncryptedAssertion found and no SP private key found on the settings to decrypt it" - assert_raises(OneLogin::RubySaml::ValidationError, error_msg) do + assert_raises(RubySaml::ValidationError, error_msg) do response.send(:decrypt_assertion, encrypted_assertion_node) end end @@ -1569,13 +1569,13 @@ def generate_audience_error(expected, actual) it "is not possible to decrypt the assertion if private key has expired and :check_sp_expiration is true" do settings.certificate = ruby_saml_cert_text settings.security[:check_sp_cert_expiration] = true - assert_raises(OneLogin::RubySaml::ValidationError, "The SP certificate expired.") do - OneLogin::RubySaml::Response.new(signed_message_encrypted_unsigned_assertion, :settings => settings) + assert_raises(RubySaml::ValidationError, "The SP certificate expired.") do + RubySaml::Response.new(signed_message_encrypted_unsigned_assertion, :settings => settings) end end it "is possible to decrypt the assertion if private key" do - response = OneLogin::RubySaml::Response.new(signed_message_encrypted_unsigned_assertion, :settings => settings) + response = RubySaml::Response.new(signed_message_encrypted_unsigned_assertion, :settings => settings) encrypted_assertion_node = REXML::XPath.first( response.document, @@ -1601,7 +1601,7 @@ def generate_audience_error(expected, actual) { certificate: ruby_saml_cert_text, private_key: ruby_saml_key_text } ] } - response = OneLogin::RubySaml::Response.new(signed_message_encrypted_unsigned_assertion, :settings => settings) + response = RubySaml::Response.new(signed_message_encrypted_unsigned_assertion, :settings => settings) encrypted_assertion_node = REXML::XPath.first( response.document, @@ -1616,7 +1616,7 @@ def generate_audience_error(expected, actual) it "is possible to decrypt the assertion if private key provided and EncryptedKey RetrievalMethod presents in response" do settings.private_key = ruby_saml_key_text resp = read_response('response_with_retrieval_method.xml') - response = OneLogin::RubySaml::Response.new(resp, :settings => settings) + response = RubySaml::Response.new(resp, :settings => settings) encrypted_assertion_node = REXML::XPath.first( response.document, @@ -1637,7 +1637,7 @@ def generate_audience_error(expected, actual) it "is possible to decrypt the assertion if private key but no saml namespace on the Assertion Element that is inside the EncryptedAssertion" do unsigned_message_encrypted_assertion_without_saml_namespace = read_response('unsigned_message_encrypted_assertion_without_saml_namespace.xml.base64') - response = OneLogin::RubySaml::Response.new(unsigned_message_encrypted_assertion_without_saml_namespace, :settings => settings) + response = RubySaml::Response.new(unsigned_message_encrypted_assertion_without_saml_namespace, :settings => settings) encrypted_assertion_node = REXML::XPath.first( response.document, "(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)", @@ -1658,28 +1658,28 @@ def generate_audience_error(expected, actual) describe "check different encrypt methods supported" do it "EncryptionMethod DES-192 && Key Encryption Algorithm RSA-1_5" do unsigned_message_des192_encrypted_signed_assertion = read_response('unsigned_message_des192_encrypted_signed_assertion.xml.base64') - response = OneLogin::RubySaml::Response.new(unsigned_message_des192_encrypted_signed_assertion, :settings => settings) + response = RubySaml::Response.new(unsigned_message_des192_encrypted_signed_assertion, :settings => settings) assert_equal "test", response.attributes[:uid] assert_equal "_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7", response.nameid end it "EncryptionMethod AES-128 && Key Encryption Algorithm RSA-OAEP-MGF1P" do unsigned_message_aes128_encrypted_signed_assertion = read_response('unsigned_message_aes128_encrypted_signed_assertion.xml.base64') - response = OneLogin::RubySaml::Response.new(unsigned_message_aes128_encrypted_signed_assertion, :settings => settings) + response = RubySaml::Response.new(unsigned_message_aes128_encrypted_signed_assertion, :settings => settings) assert_equal "test", response.attributes[:uid] assert_equal "_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7", response.nameid end it "EncryptionMethod AES-192 && Key Encryption Algorithm RSA-OAEP-MGF1P" do unsigned_message_aes192_encrypted_signed_assertion = read_response('unsigned_message_aes192_encrypted_signed_assertion.xml.base64') - response = OneLogin::RubySaml::Response.new(unsigned_message_aes192_encrypted_signed_assertion, :settings => settings) + response = RubySaml::Response.new(unsigned_message_aes192_encrypted_signed_assertion, :settings => settings) assert_equal "test", response.attributes[:uid] assert_equal "_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7", response.nameid end it "EncryptionMethod AES-256 && Key Encryption Algorithm RSA-OAEP-MGF1P" do unsigned_message_aes256_encrypted_signed_assertion = read_response('unsigned_message_aes256_encrypted_signed_assertion.xml.base64') - response = OneLogin::RubySaml::Response.new(unsigned_message_aes256_encrypted_signed_assertion, :settings => settings) + response = RubySaml::Response.new(unsigned_message_aes256_encrypted_signed_assertion, :settings => settings) assert_equal "test", response.attributes[:uid] assert_equal "_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7", response.nameid end @@ -1687,7 +1687,7 @@ def generate_audience_error(expected, actual) it "EncryptionMethod AES-128-GCM && Key Encryption Algorithm RSA-OAEP-MGF1P" do return unless OpenSSL::Cipher.ciphers.include? 'AES-128-GCM' unsigned_message_aes128gcm_encrypted_signed_assertion = read_response('unsigned_message_aes128gcm_encrypted_signed_assertion.xml.base64') - response = OneLogin::RubySaml::Response.new(unsigned_message_aes128gcm_encrypted_signed_assertion, :settings => settings) + response = RubySaml::Response.new(unsigned_message_aes128gcm_encrypted_signed_assertion, :settings => settings) assert_equal "test", response.attributes[:uid] assert_equal "_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7", response.nameid end @@ -1695,7 +1695,7 @@ def generate_audience_error(expected, actual) it "EncryptionMethod AES-192-GCM && Key Encryption Algorithm RSA-OAEP-MGF1P" do return unless OpenSSL::Cipher.ciphers.include? 'AES-192-GCM' unsigned_message_aes192gcm_encrypted_signed_assertion = read_response('unsigned_message_aes192gcm_encrypted_signed_assertion.xml.base64') - response = OneLogin::RubySaml::Response.new(unsigned_message_aes192gcm_encrypted_signed_assertion, :settings => settings) + response = RubySaml::Response.new(unsigned_message_aes192gcm_encrypted_signed_assertion, :settings => settings) assert_equal "test", response.attributes[:uid] assert_equal "_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7", response.nameid end @@ -1703,7 +1703,7 @@ def generate_audience_error(expected, actual) it "EncryptionMethod AES-256-GCM && Key Encryption Algorithm RSA-OAEP-MGF1P" do return unless OpenSSL::Cipher.ciphers.include? 'AES-256-GCM' unsigned_message_aes256gcm_encrypted_signed_assertion = read_response('unsigned_message_aes256gcm_encrypted_signed_assertion.xml.base64') - response = OneLogin::RubySaml::Response.new(unsigned_message_aes256gcm_encrypted_signed_assertion, :settings => settings) + response = RubySaml::Response.new(unsigned_message_aes256gcm_encrypted_signed_assertion, :settings => settings) assert_equal "test", response.attributes[:uid] assert_equal "_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7", response.nameid end @@ -1723,7 +1723,7 @@ def generate_audience_error(expected, actual) describe "test qualified name id in attributes" do it "parsed the nameid" do - response = OneLogin::RubySaml::Response.new(read_response("signed_nameid_in_atts.xml"), :settings => settings) + response = RubySaml::Response.new(read_response("signed_nameid_in_atts.xml"), :settings => settings) response.settings.idp_cert_fingerprint = 'c51985d947f1be57082025050846eb27f6cab783' assert_empty response.errors assert_equal "test", response.attributes[:uid] @@ -1734,7 +1734,7 @@ def generate_audience_error(expected, actual) describe "test unqualified name id in attributes" do it "parsed the nameid" do - response = OneLogin::RubySaml::Response.new(read_response("signed_unqual_nameid_in_atts.xml"), :settings => settings) + response = RubySaml::Response.new(read_response("signed_unqual_nameid_in_atts.xml"), :settings => settings) response.settings.idp_cert_fingerprint = 'c51985d947f1be57082025050846eb27f6cab783' assert_empty response.errors assert_equal "test", response.attributes[:uid] @@ -1746,7 +1746,7 @@ def generate_audience_error(expected, actual) it "should not be valid" do settings.private_key = ruby_saml_key_text signature_wrapping_attack = read_invalid_response("encrypted_new_attack.xml.base64") - response_wrapped = OneLogin::RubySaml::Response.new(signature_wrapping_attack, :settings => settings) + response_wrapped = RubySaml::Response.new(signature_wrapping_attack, :settings => settings) response_wrapped.stubs(:conditions).returns(nil) response_wrapped.stubs(:validate_subject_confirmation).returns(true) settings.idp_cert_fingerprint = "385b1eec71143f00db6af936e2ea12a28771d72c" @@ -1758,7 +1758,7 @@ def generate_audience_error(expected, actual) describe "signature wrapping attack - concealed SAML response body" do it "should not be valid" do signature_wrapping_attack = read_invalid_response("response_with_concealed_signed_assertion.xml") - response_wrapped = OneLogin::RubySaml::Response.new(signature_wrapping_attack, :settings => settings) + response_wrapped = RubySaml::Response.new(signature_wrapping_attack, :settings => settings) settings.idp_cert_fingerprint = '4b68c453c7d994aad9025c99d5efcf566287fe8d' response_wrapped.stubs(:conditions).returns(nil) response_wrapped.stubs(:validate_subject_confirmation).returns(true) @@ -1770,7 +1770,7 @@ def generate_audience_error(expected, actual) describe "signature wrapping attack - doubled signed assertion SAML response" do it "should not be valid" do signature_wrapping_attack = read_invalid_response("response_with_doubled_signed_assertion.xml") - response_wrapped = OneLogin::RubySaml::Response.new(signature_wrapping_attack, :settings => settings) + response_wrapped = RubySaml::Response.new(signature_wrapping_attack, :settings => settings) settings.idp_cert_fingerprint = '4b68c453c7d994aad9025c99d5efcf566287fe8d' response_wrapped.stubs(:conditions).returns(nil) response_wrapped.stubs(:validate_subject_confirmation).returns(true) diff --git a/test/saml_message_test.rb b/test/saml_message_test.rb index 83f36768b..56dc8f6a1 100644 --- a/test/saml_message_test.rb +++ b/test/saml_message_test.rb @@ -4,8 +4,8 @@ class RubySamlTest < Minitest::Test describe "SamlMessage" do - let(:settings) { OneLogin::RubySaml::Settings.new } - let(:saml_message) { OneLogin::RubySaml::SamlMessage.new } + let(:settings) { RubySaml::Settings.new } + let(:saml_message) { RubySaml::SamlMessage.new } let(:response_document) { read_response("response_unsigned_xml_base64") } let(:response_document_xml) { read_response("adfs_response_xmlns.xml") } @@ -64,19 +64,19 @@ class RubySamlTest < Minitest::Test """ } it "raises error when SAML Message exceed the allowed bytes" do - assert_raises(OneLogin::RubySaml::ValidationError, "Encoded SAML Message exceeds #{OneLogin::RubySaml::Settings::DEFAULTS[:message_max_bytesize]} bytes, so was rejected") do - saml_message = OneLogin::RubySaml::SamlMessage.new + assert_raises(RubySaml::ValidationError, "Encoded SAML Message exceeds #{RubySaml::Settings::DEFAULTS[:message_max_bytesize]} bytes, so was rejected") do + saml_message = RubySaml::SamlMessage.new saml_message.send(:decode_raw_saml, bomb) end end describe 'with a custom setting for message_max_bytesize' do let(:message_max_bytesize) { 100_00 } - let(:settings) { OneLogin::RubySaml::Settings.new({:message_max_bytesize => message_max_bytesize}) } + let(:settings) { RubySaml::Settings.new({:message_max_bytesize => message_max_bytesize}) } it 'uses the custom setting' do - assert_raises(OneLogin::RubySaml::ValidationError, "Encoded SAML Message exceeds #{message_max_bytesize} bytes, so was rejected") do - saml_message = OneLogin::RubySaml::SamlMessage.new + assert_raises(RubySaml::ValidationError, "Encoded SAML Message exceeds #{message_max_bytesize} bytes, so was rejected") do + saml_message = RubySaml::SamlMessage.new saml_message.send(:decode_raw_saml, bomb, settings) end end diff --git a/test/settings_test.rb b/test/settings_test.rb index 60672130c..a0ca46faa 100644 --- a/test/settings_test.rb +++ b/test/settings_test.rb @@ -7,7 +7,7 @@ class SettingsTest < Minitest::Test describe "Settings" do before do - @settings = OneLogin::RubySaml::Settings.new + @settings = RubySaml::Settings.new end it "should provide getters and settings" do @@ -96,7 +96,7 @@ class SettingsTest < Minitest::Test :passive => true, :protocol_binding => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' } - @settings = OneLogin::RubySaml::Settings.new(config) + @settings = RubySaml::Settings.new(config) config.each do |k,v| assert_equal v, @settings.send(k) @@ -115,13 +115,13 @@ class SettingsTest < Minitest::Test end it "does not modify default security settings" do - settings = OneLogin::RubySaml::Settings.new + settings = RubySaml::Settings.new settings.security[:authn_requests_signed] = true settings.security[:embed_sign] = true settings.security[:digest_method] = XMLSecurity::Document::SHA256 settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA256 - new_settings = OneLogin::RubySaml::Settings.new + new_settings = RubySaml::Settings.new assert_equal new_settings.security[:authn_requests_signed], false assert_equal new_settings.security[:embed_sign], false assert_equal new_settings.security[:digest_method], XMLSecurity::Document::SHA1 @@ -135,9 +135,9 @@ class SettingsTest < Minitest::Test } } - @default_attributes = OneLogin::RubySaml::Settings::DEFAULTS + @default_attributes = RubySaml::Settings::DEFAULTS - @settings = OneLogin::RubySaml::Settings.new(config, true) + @settings = RubySaml::Settings.new(config, true) assert_equal @settings.security[:metadata_signed], true assert_equal @settings.security[:digest_method], @default_attributes[:security][:digest_method] end @@ -149,9 +149,9 @@ class SettingsTest < Minitest::Test } } - @default_attributes = OneLogin::RubySaml::Settings::DEFAULTS + @default_attributes = RubySaml::Settings::DEFAULTS - @settings = OneLogin::RubySaml::Settings.new(config) + @settings = RubySaml::Settings.new(config) assert_equal @settings.security[:metadata_signed], true assert_nil @settings.security[:digest_method] end @@ -355,7 +355,7 @@ class SettingsTest < Minitest::Test it "raises an error if SP certificate expired and check_sp_cert_expiration enabled" do @settings.certificate = ruby_saml_cert_text @settings.security[:check_sp_cert_expiration] = true - assert_raises(OneLogin::RubySaml::ValidationError) { @settings.get_sp_cert } + assert_raises(RubySaml::ValidationError) { @settings.get_sp_cert } end end @@ -623,7 +623,7 @@ class SettingsTest < Minitest::Test @settings.security = { check_sp_cert_expiration: true } @settings.sp_cert_multi = { signing: [expired_pair], encryption: [valid_pair] } - assert_raises OneLogin::RubySaml::ValidationError do + assert_raises RubySaml::ValidationError do @settings.get_sp_certs end end @@ -632,7 +632,7 @@ class SettingsTest < Minitest::Test @settings.security = { check_sp_cert_expiration: true } @settings.sp_cert_multi = { signing: [valid_pair], encryption: [expired_pair] } - assert_raises OneLogin::RubySaml::ValidationError do + assert_raises RubySaml::ValidationError do @settings.get_sp_certs end end @@ -678,11 +678,11 @@ class SettingsTest < Minitest::Test @settings.security = { check_sp_cert_expiration: true } @settings.sp_cert_multi = { signing: [early_pair, expired] } - assert_raises OneLogin::RubySaml::ValidationError do + assert_raises RubySaml::ValidationError do @settings.get_sp_signing_pair end - assert_raises OneLogin::RubySaml::ValidationError do + assert_raises RubySaml::ValidationError do @settings.get_sp_signing_key end end @@ -721,7 +721,7 @@ class SettingsTest < Minitest::Test @settings.security = { check_sp_cert_expiration: true } @settings.sp_cert_multi = { encryption: [early_pair, expired_pair] } - assert_raises OneLogin::RubySaml::ValidationError do + assert_raises RubySaml::ValidationError do @settings.get_sp_decryption_keys end end diff --git a/test/slo_logoutrequest_test.rb b/test/slo_logoutrequest_test.rb index f8fca39ef..3ae13c269 100644 --- a/test/slo_logoutrequest_test.rb +++ b/test/slo_logoutrequest_test.rb @@ -8,10 +8,10 @@ class RubySamlTest < Minitest::Test describe "SloLogoutrequest" do - let(:settings) { OneLogin::RubySaml::Settings.new } - let(:logout_request) { OneLogin::RubySaml::SloLogoutrequest.new(logout_request_document) } - let(:logout_request_encrypted_nameid) { OneLogin::RubySaml::SloLogoutrequest.new(logout_request_encrypted_nameid_document) } - let(:invalid_logout_request) { OneLogin::RubySaml::SloLogoutrequest.new(invalid_logout_request_document) } + let(:settings) { RubySaml::Settings.new } + let(:logout_request) { RubySaml::SloLogoutrequest.new(logout_request_document) } + let(:logout_request_encrypted_nameid) { RubySaml::SloLogoutrequest.new(logout_request_encrypted_nameid_document) } + let(:invalid_logout_request) { RubySaml::SloLogoutrequest.new(invalid_logout_request_document) } before do settings.idp_entity_id = 'https://app.onelogin.com/saml/metadata/SOMEACCOUNT' @@ -22,13 +22,13 @@ class RubySamlTest < Minitest::Test describe "initiator" do it "raise an exception when logout request is initialized with nil" do - assert_raises(ArgumentError) { OneLogin::RubySaml::SloLogoutrequest.new(nil) } + assert_raises(ArgumentError) { RubySaml::SloLogoutrequest.new(nil) } end end describe "#is_valid?" do it "return false when logout request is initialized with blank data" do - logout_request_blank = OneLogin::RubySaml::SloLogoutrequest.new('') + logout_request_blank = RubySaml::SloLogoutrequest.new('') assert !logout_request_blank.is_valid? assert_includes logout_request_blank.errors, 'Blank logout request' end @@ -68,7 +68,7 @@ class RubySamlTest < Minitest::Test options = {} options[:get_params] = params - logout_request_sign_test = OneLogin::RubySaml::SloLogoutrequest.new(params['SAMLRequest'], options) + logout_request_sign_test = RubySaml::SloLogoutrequest.new(params['SAMLRequest'], options) logout_request_sign_test.settings = settings collect_errors = true @@ -79,7 +79,7 @@ class RubySamlTest < Minitest::Test it "raise error for invalid xml" do invalid_logout_request.soft = false - assert_raises(OneLogin::RubySaml::ValidationError) { invalid_logout_request.is_valid? } + assert_raises(RubySaml::ValidationError) { invalid_logout_request.is_valid? } end end @@ -90,7 +90,7 @@ class RubySamlTest < Minitest::Test end it 'is not possible when encryptID but no private key' do - assert_raises(OneLogin::RubySaml::ValidationError, "An EncryptedID found and no SP private key found on the settings to decrypt it") do + assert_raises(RubySaml::ValidationError, "An EncryptedID found and no SP private key found on the settings to decrypt it") do assert_equal "someone@example.org", logout_request_encrypted_nameid.nameid end end @@ -103,14 +103,14 @@ class RubySamlTest < Minitest::Test end describe "#nameid_format" do - let(:logout_request) { OneLogin::RubySaml::SloLogoutrequest.new(logout_request_document_with_name_id_format) } + let(:logout_request) { RubySaml::SloLogoutrequest.new(logout_request_document_with_name_id_format) } it "extract the format attribute of the name id element" do assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", logout_request.nameid_format end it 'is not possible when encryptID but no private key' do - assert_raises(OneLogin::RubySaml::ValidationError, "An EncryptedID found and no SP private key found on the settings to decrypt it") do + assert_raises(RubySaml::ValidationError, "An EncryptedID found and no SP private key found on the settings to decrypt it") do assert_equal "someone@example.org", logout_request_encrypted_nameid.nameid end end @@ -149,7 +149,7 @@ class RubySamlTest < Minitest::Test end it "return an Array with one SessionIndex" do - logout_request_with_session_index = OneLogin::RubySaml::SloLogoutrequest.new(logout_request_xml_with_session_index) + logout_request_with_session_index = RubySaml::SloLogoutrequest.new(logout_request_xml_with_session_index) assert_equal ['_ea853497-c58a-408a-bc23-c849752d9741'], logout_request_with_session_index.session_indexes end end @@ -161,7 +161,7 @@ class RubySamlTest < Minitest::Test end it "return false when there is an invalid ID in the logout request" do - logout_request_blank = OneLogin::RubySaml::SloLogoutrequest.new('') + logout_request_blank = RubySaml::SloLogoutrequest.new('') assert !logout_request_blank.send(:validate_id) assert_includes logout_request_blank.errors, "Missing ID attribute on Logout Request" end @@ -173,7 +173,7 @@ class RubySamlTest < Minitest::Test end it "return false when the logout request is not SAML 2.0 Version" do - logout_request_blank = OneLogin::RubySaml::SloLogoutrequest.new('') + logout_request_blank = RubySaml::SloLogoutrequest.new('') assert !logout_request_blank.send(:validate_version) assert_includes logout_request_blank.errors, "Unsupported SAML version" end @@ -203,7 +203,7 @@ class RubySamlTest < Minitest::Test Timecop.freeze Time.parse('2014-07-17T01:01:49Z') do logout_request.document.root.attributes['NotOnOrAfter'] = '2014-07-17T01:01:48Z' logout_request.soft = false - assert_raises(OneLogin::RubySaml::ValidationError, "Current time is on or after NotOnOrAfter") do + assert_raises(RubySaml::ValidationError, "Current time is on or after NotOnOrAfter") do logout_request.send(:validate_not_on_or_after) end end @@ -242,16 +242,16 @@ class RubySamlTest < Minitest::Test end it "return false when invalid logout request xml" do - logout_request_blank = OneLogin::RubySaml::SloLogoutrequest.new('') + logout_request_blank = RubySaml::SloLogoutrequest.new('') logout_request_blank.soft = true assert !logout_request_blank.send(:validate_request_state) assert_includes logout_request_blank.errors, "Blank logout request" end it "raise error for invalid xml" do - logout_request_blank = OneLogin::RubySaml::SloLogoutrequest.new('') + logout_request_blank = RubySaml::SloLogoutrequest.new('') logout_request_blank.soft = false - assert_raises(OneLogin::RubySaml::ValidationError, "Blank logout request") do + assert_raises(RubySaml::ValidationError, "Blank logout request") do logout_request_blank.send(:validate_request_state) end end @@ -270,7 +270,7 @@ class RubySamlTest < Minitest::Test it "raise when encountering a Logout Request bad formatted" do invalid_logout_request.soft = false - assert_raises(OneLogin::RubySaml::ValidationError, "Element '{urn:oasis:names:tc:SAML:2.0:assertion}Issuer': This element is not expected") do + assert_raises(RubySaml::ValidationError, "Element '{urn:oasis:names:tc:SAML:2.0:assertion}Issuer': This element is not expected") do invalid_logout_request.send(:validate_structure) end end @@ -291,7 +291,7 @@ class RubySamlTest < Minitest::Test it "raise when the issuer of the Logout Request does not match the IdP entityId" do logout_request.settings.idp_entity_id = 'http://idp.example.com/invalid' logout_request.soft = false - assert_raises(OneLogin::RubySaml::ValidationError, "Doesn't match the issuer, expected: <#{logout_request.settings.idp_entity_id}>, but was: ") do + assert_raises(RubySaml::ValidationError, "Doesn't match the issuer, expected: <#{logout_request.settings.idp_entity_id}>, but was: ") do logout_request.send(:validate_issuer) end end @@ -308,12 +308,12 @@ class RubySamlTest < Minitest::Test it "return true when no idp_cert is provided and option :relax_signature_validation is present" do settings.idp_cert = nil settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA1 - params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') + params = RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') params['RelayState'] = params[:RelayState] options = {} options[:get_params] = params options[:relax_signature_validation] = true - logout_request_sign_test = OneLogin::RubySaml::SloLogoutrequest.new(params['SAMLRequest'], options) + logout_request_sign_test = RubySaml::SloLogoutrequest.new(params['SAMLRequest'], options) logout_request_sign_test.settings = settings assert logout_request_sign_test.send(:validate_signature) end @@ -321,32 +321,32 @@ class RubySamlTest < Minitest::Test it "return false when no idp_cert is provided and no option :relax_signature_validation is present" do settings.idp_cert = nil settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA1 - params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') + params = RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') params['RelayState'] = params[:RelayState] options = {} options[:get_params] = params - logout_request_sign_test = OneLogin::RubySaml::SloLogoutrequest.new(params['SAMLRequest'], options) + logout_request_sign_test = RubySaml::SloLogoutrequest.new(params['SAMLRequest'], options) logout_request_sign_test.settings = settings assert !logout_request_sign_test.send(:validate_signature) end it "return true when valid RSA_SHA1 Signature" do settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA1 - params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') + params = RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') params['RelayState'] = params[:RelayState] options = {} options[:get_params] = params - logout_request_sign_test = OneLogin::RubySaml::SloLogoutrequest.new(params['SAMLRequest'], options) + logout_request_sign_test = RubySaml::SloLogoutrequest.new(params['SAMLRequest'], options) logout_request_sign_test.settings = settings assert logout_request_sign_test.send(:validate_signature) end it "return true when valid RSA_SHA256 Signature" do settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA256 - params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') + params = RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') options = {} options[:get_params] = params - logout_request_sign_test = OneLogin::RubySaml::SloLogoutrequest.new(params['SAMLRequest'], options) + logout_request_sign_test = RubySaml::SloLogoutrequest.new(params['SAMLRequest'], options) params['RelayState'] = params[:RelayState] logout_request_sign_test.settings = settings assert logout_request_sign_test.send(:validate_signature) @@ -354,13 +354,13 @@ class RubySamlTest < Minitest::Test it "return false when invalid RSA_SHA1 Signature" do settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA1 - params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') + params = RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') params['RelayState'] = 'http://invalid.example.com' params[:RelayState] = params['RelayState'] options = {} options[:get_params] = params - logout_request_sign_test = OneLogin::RubySaml::SloLogoutrequest.new(params['SAMLRequest'], options) + logout_request_sign_test = RubySaml::SloLogoutrequest.new(params['SAMLRequest'], options) logout_request_sign_test.settings = settings assert !logout_request_sign_test.send(:validate_signature) end @@ -368,15 +368,15 @@ class RubySamlTest < Minitest::Test it "raise when invalid RSA_SHA1 Signature" do settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA1 settings.soft = false - params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') + params = RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') params['RelayState'] = 'http://invalid.example.com' params[:RelayState] = params['RelayState'] options = {} options[:get_params] = params options[:settings] = settings - logout_request_sign_test = OneLogin::RubySaml::SloLogoutrequest.new(params['SAMLRequest'], options) - assert_raises(OneLogin::RubySaml::ValidationError, "Invalid Signature on Logout Request") do + logout_request_sign_test = RubySaml::SloLogoutrequest.new(params['SAMLRequest'], options) + assert_raises(RubySaml::ValidationError, "Invalid Signature on Logout Request") do logout_request_sign_test.send(:validate_signature) end end @@ -385,9 +385,9 @@ class RubySamlTest < Minitest::Test # Use Logoutrequest only to build the SAMLRequest parameter. settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA1 settings.soft = false - params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings, "RelayState" => "http://example.com") + params = RubySaml::Logoutrequest.new.create_params(settings, "RelayState" => "http://example.com") # Assemble query string. - query = OneLogin::RubySaml::Utils.build_query( + query = RubySaml::Utils.build_query( :type => 'SAMLRequest', :data => params['SAMLRequest'], :relay_state => params['RelayState'], @@ -410,8 +410,8 @@ class RubySamlTest < Minitest::Test options = {} options[:get_params] = params options[:settings] = settings - logout_request_sign_test = OneLogin::RubySaml::SloLogoutrequest.new(params['SAMLRequest'], options) - assert_raises(OneLogin::RubySaml::ValidationError, "Invalid Signature on Logout Request") do + logout_request_sign_test = RubySaml::SloLogoutrequest.new(params['SAMLRequest'], options) + assert_raises(RubySaml::ValidationError, "Invalid Signature on Logout Request") do logout_request_sign_test.send(:validate_signature) end end @@ -420,9 +420,9 @@ class RubySamlTest < Minitest::Test # Use Logoutrequest only to build the SAMLRequest parameter. settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA1 settings.soft = false - params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings, "RelayState" => "http://example.com") + params = RubySaml::Logoutrequest.new.create_params(settings, "RelayState" => "http://example.com") # Assemble query string. - query = OneLogin::RubySaml::Utils.build_query( + query = RubySaml::Utils.build_query( :type => 'SAMLRequest', :data => params['SAMLRequest'], :relay_state => params['RelayState'], @@ -449,7 +449,7 @@ class RubySamlTest < Minitest::Test "RelayState" => "http%3A%2F%2Fex%61mple.com", } options[:settings] = settings - logout_request_sign_test = OneLogin::RubySaml::SloLogoutrequest.new(params['SAMLRequest'], options) + logout_request_sign_test = RubySaml::SloLogoutrequest.new(params['SAMLRequest'], options) assert logout_request_sign_test.send(:validate_signature) end @@ -459,8 +459,8 @@ class RubySamlTest < Minitest::Test settings.soft = false # Creating the query manually to tweak it later instead of using - # OneLogin::RubySaml::Utils.build_query - request_doc = OneLogin::RubySaml::Logoutrequest.new.create_logout_request_xml_doc(settings) + # RubySaml::Utils.build_query + request_doc = RubySaml::Logoutrequest.new.create_logout_request_xml_doc(settings) request = Zlib::Deflate.deflate(request_doc.to_s, 9)[2..-5] base64_request = Base64.encode64(request).gsub(/\n/, "") # The original request received from Azure AD comes with downcased @@ -483,17 +483,17 @@ class RubySamlTest < Minitest::Test params = params.map { |k, v| [k, CGI.unescape(v)] }.to_h # Construct SloLogoutrequest and ask it to validate the signature. # It will fail because the signature is based on the downcased request - logout_request_downcased_test = OneLogin::RubySaml::SloLogoutrequest.new( + logout_request_downcased_test = RubySaml::SloLogoutrequest.new( params['SAMLRequest'], get_params: params, settings: settings, ) - assert_raises(OneLogin::RubySaml::ValidationError, "Invalid Signature on Logout Request") do + assert_raises(RubySaml::ValidationError, "Invalid Signature on Logout Request") do logout_request_downcased_test.send(:validate_signature) end # For this case, the parameters will be forced to be downcased after # being escaped with :lowercase_url_encoding security option settings.security[:lowercase_url_encoding] = true - logout_request_force_downcasing_test = OneLogin::RubySaml::SloLogoutrequest.new( + logout_request_force_downcasing_test = RubySaml::SloLogoutrequest.new( params['SAMLRequest'], get_params: params, settings: settings ) assert logout_request_force_downcasing_test.send(:validate_signature) @@ -510,11 +510,11 @@ class RubySamlTest < Minitest::Test end it "return true when at least a idp_cert is valid" do - params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') + params = RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') params['RelayState'] = params[:RelayState] options = {} options[:get_params] = params - logout_request_sign_test = OneLogin::RubySaml::SloLogoutrequest.new(params['SAMLRequest'], options) + logout_request_sign_test = RubySaml::SloLogoutrequest.new(params['SAMLRequest'], options) settings.idp_cert_multi = { :signing => [ruby_saml_cert_text2, ruby_saml_cert_text], :encryption => [] @@ -524,12 +524,12 @@ class RubySamlTest < Minitest::Test end it "return false when cert expired and check_idp_cert_expiration expired" do - params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') + params = RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') params['RelayState'] = params[:RelayState] options = {} options[:get_params] = params settings.security[:check_idp_cert_expiration] = true - logout_request_sign_test = OneLogin::RubySaml::SloLogoutrequest.new(params['SAMLRequest'], options) + logout_request_sign_test = RubySaml::SloLogoutrequest.new(params['SAMLRequest'], options) settings.idp_cert = nil settings.idp_cert_multi = { :signing => [ruby_saml_cert_text], @@ -541,11 +541,11 @@ class RubySamlTest < Minitest::Test end it "return false when none cert on idp_cert_multi is valid" do - params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') + params = RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com') params['RelayState'] = params[:RelayState] options = {} options[:get_params] = params - logout_request_sign_test = OneLogin::RubySaml::SloLogoutrequest.new(params['SAMLRequest'], options) + logout_request_sign_test = RubySaml::SloLogoutrequest.new(params['SAMLRequest'], options) settings.idp_cert_fingerprint = ruby_saml_cert_fingerprint settings.idp_cert_multi = { :signing => [ruby_saml_cert_text2, ruby_saml_cert_text2], diff --git a/test/slo_logoutresponse_test.rb b/test/slo_logoutresponse_test.rb index a1696d24d..43441e3d5 100644 --- a/test/slo_logoutresponse_test.rb +++ b/test/slo_logoutresponse_test.rb @@ -5,8 +5,8 @@ class SloLogoutresponseTest < Minitest::Test describe "SloLogoutresponse" do - let(:settings) { OneLogin::RubySaml::Settings.new } - let(:logout_request) { OneLogin::RubySaml::SloLogoutrequest.new(logout_request_document) } + let(:settings) { RubySaml::Settings.new } + let(:logout_request) { RubySaml::SloLogoutrequest.new(logout_request_document) } before do settings.idp_entity_id = 'https://app.onelogin.com/saml/metadata/SOMEACCOUNT' @@ -19,7 +19,7 @@ class SloLogoutresponseTest < Minitest::Test end it "create the deflated SAMLResponse URL parameter" do - unauth_url = OneLogin::RubySaml::SloLogoutresponse.new.create(settings, logout_request.id) + unauth_url = RubySaml::SloLogoutresponse.new.create(settings, logout_request.id) assert_match(/^http:\/\/unauth\.com\/logout\?SAMLResponse=/, unauth_url) inflated = decode_saml_response_payload(unauth_url) @@ -27,46 +27,46 @@ class SloLogoutresponseTest < Minitest::Test end it "support additional params" do - unauth_url = OneLogin::RubySaml::SloLogoutresponse.new.create(settings, logout_request.id, nil, { :hello => nil }) + unauth_url = RubySaml::SloLogoutresponse.new.create(settings, logout_request.id, nil, { :hello => nil }) assert_match(/&hello=$/, unauth_url) - unauth_url = OneLogin::RubySaml::SloLogoutresponse.new.create(settings, logout_request.id, nil, { :foo => "bar" }) + unauth_url = RubySaml::SloLogoutresponse.new.create(settings, logout_request.id, nil, { :foo => "bar" }) assert_match(/&foo=bar$/, unauth_url) - unauth_url = OneLogin::RubySaml::SloLogoutresponse.new.create(settings, logout_request.id, nil, { :RelayState => "http://idp.example.com" }) + unauth_url = RubySaml::SloLogoutresponse.new.create(settings, logout_request.id, nil, { :RelayState => "http://idp.example.com" }) assert_match(/&RelayState=http%3A%2F%2Fidp.example.com$/, unauth_url) end it "RelayState cases" do - unauth_url = OneLogin::RubySaml::SloLogoutresponse.new.create(settings, logout_request.id, nil, { :RelayState => nil }) + unauth_url = RubySaml::SloLogoutresponse.new.create(settings, logout_request.id, nil, { :RelayState => nil }) assert !unauth_url.include?('RelayState') - unauth_url = OneLogin::RubySaml::SloLogoutresponse.new.create(settings, logout_request.id, nil, { :RelayState => "http://example.com" }) + unauth_url = RubySaml::SloLogoutresponse.new.create(settings, logout_request.id, nil, { :RelayState => "http://example.com" }) assert unauth_url.include?('&RelayState=http%3A%2F%2Fexample.com') - unauth_url = OneLogin::RubySaml::SloLogoutresponse.new.create(settings, logout_request.id, nil, { 'RelayState' => nil }) + unauth_url = RubySaml::SloLogoutresponse.new.create(settings, logout_request.id, nil, { 'RelayState' => nil }) assert !unauth_url.include?('RelayState') - unauth_url = OneLogin::RubySaml::SloLogoutresponse.new.create(settings, logout_request.id, nil, { 'RelayState' => "http://example.com" }) + unauth_url = RubySaml::SloLogoutresponse.new.create(settings, logout_request.id, nil, { 'RelayState' => "http://example.com" }) assert unauth_url.include?('&RelayState=http%3A%2F%2Fexample.com') end it "set InResponseTo to the ID from the logout request" do - unauth_url = OneLogin::RubySaml::SloLogoutresponse.new.create(settings, logout_request.id) + unauth_url = RubySaml::SloLogoutresponse.new.create(settings, logout_request.id) inflated = decode_saml_response_payload(unauth_url) assert_match(/InResponseTo='_c0348950-935b-0131-1060-782bcb56fcaa'/, inflated) end it "set a custom successful logout message on the response" do - unauth_url = OneLogin::RubySaml::SloLogoutresponse.new.create(settings, logout_request.id, "Custom Logout Message") + unauth_url = RubySaml::SloLogoutresponse.new.create(settings, logout_request.id, "Custom Logout Message") inflated = decode_saml_response_payload(unauth_url) assert_match(/Custom Logout Message<\/samlp:StatusMessage>/, inflated) end it "set a custom logout message and an status on the response" do - unauth_url = OneLogin::RubySaml::SloLogoutresponse.new.create(settings, nil, "Custom Logout Message", {}, "urn:oasis:names:tc:SAML:2.0:status:PartialLogout") + unauth_url = RubySaml::SloLogoutresponse.new.create(settings, nil, "Custom Logout Message", {}, "urn:oasis:names:tc:SAML:2.0:status:PartialLogout") inflated = decode_saml_response_payload(unauth_url) assert_match(/Custom Logout Message<\/samlp:StatusMessage>/, inflated) @@ -76,7 +76,7 @@ class SloLogoutresponseTest < Minitest::Test it "uses the response location when set" do settings.idp_slo_response_service_url = "http://unauth.com/logout/return" - unauth_url = OneLogin::RubySaml::SloLogoutresponse.new.create(settings, logout_request.id) + unauth_url = RubySaml::SloLogoutresponse.new.create(settings, logout_request.id) assert_match(/^http:\/\/unauth\.com\/logout\/return\?SAMLResponse=/, unauth_url) inflated = decode_saml_response_payload(unauth_url) @@ -85,16 +85,16 @@ class SloLogoutresponseTest < Minitest::Test describe "playgin with preix" do it "creates request with ID prefixed with default '_'" do - request = OneLogin::RubySaml::SloLogoutresponse.new + request = RubySaml::SloLogoutresponse.new assert_match(/^_/, request.uuid) end it "creates request with ID is prefixed, when :id_prefix is passed" do - OneLogin::RubySaml::Utils::set_prefix("test") - request = OneLogin::RubySaml::SloLogoutresponse.new + RubySaml::Utils::set_prefix("test") + request = RubySaml::SloLogoutresponse.new assert_match(/^test/, request.uuid) - OneLogin::RubySaml::Utils::set_prefix("_") + RubySaml::Utils::set_prefix("_") end end @@ -107,7 +107,7 @@ class SloLogoutresponseTest < Minitest::Test end it "doesn't sign through create_xml_document" do - unauth_res = OneLogin::RubySaml::SloLogoutresponse.new + unauth_res = RubySaml::SloLogoutresponse.new inflated = unauth_res.create_xml_document(settings).to_s refute_match %r[([a-zA-Z0-9/+=]+)], inflated @@ -116,7 +116,7 @@ class SloLogoutresponseTest < Minitest::Test end it "sign unsigned request" do - unauth_res = OneLogin::RubySaml::SloLogoutresponse.new + unauth_res = RubySaml::SloLogoutresponse.new unauth_res_doc = unauth_res.create_xml_document(settings) inflated = unauth_res_doc.to_s @@ -132,7 +132,7 @@ class SloLogoutresponseTest < Minitest::Test end it "signs through create_logout_response_xml_doc" do - unauth_res = OneLogin::RubySaml::SloLogoutresponse.new + unauth_res = RubySaml::SloLogoutresponse.new inflated = unauth_res.create_logout_response_xml_doc(settings).to_s assert_match %r[([a-zA-Z0-9/+=]+)], inflated @@ -143,7 +143,7 @@ class SloLogoutresponseTest < Minitest::Test it "create a signed logout response" do logout_request.settings = settings - params = OneLogin::RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message") + params = RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message") response_xml = Base64.decode64(params["SAMLResponse"]) assert_match %r[([a-zA-Z0-9/+=]+)], response_xml @@ -156,7 +156,7 @@ class SloLogoutresponseTest < Minitest::Test settings.security[:digest_method] = XMLSecurity::Document::SHA256 logout_request.settings = settings - params = OneLogin::RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message") + params = RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message") response_xml = Base64.decode64(params["SAMLResponse"]) assert_match %r[([a-zA-Z0-9/+=]+)], response_xml @@ -169,7 +169,7 @@ class SloLogoutresponseTest < Minitest::Test settings.security[:digest_method] = XMLSecurity::Document::SHA512 logout_request.settings = settings - params = OneLogin::RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message") + params = RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message") response_xml = Base64.decode64(params["SAMLResponse"]) assert_match %r[([a-zA-Z0-9/+=]+)], response_xml @@ -188,7 +188,7 @@ class SloLogoutresponseTest < Minitest::Test } logout_request.settings = settings - params = OneLogin::RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message") + params = RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message") response_xml = Base64.decode64(params["SAMLResponse"]) assert_match %r[([a-zA-Z0-9/+=]+)], response_xml @@ -208,7 +208,7 @@ class SloLogoutresponseTest < Minitest::Test } logout_request.settings = settings - params = OneLogin::RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message") + params = RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message") response_xml = Base64.decode64(params["SAMLResponse"]) assert_match %r[([a-zA-Z0-9/+=]+)], response_xml @@ -220,8 +220,8 @@ class SloLogoutresponseTest < Minitest::Test settings.security[:check_sp_cert_expiration] = true logout_request.settings = settings - assert_raises(OneLogin::RubySaml::ValidationError, 'The SP certificate expired.') do - OneLogin::RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message") + assert_raises(RubySaml::ValidationError, 'The SP certificate expired.') do + RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message") end end end @@ -239,7 +239,7 @@ class SloLogoutresponseTest < Minitest::Test it "create a signature parameter with RSA_SHA1 and validate it" do settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA1 - params = OneLogin::RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message", :RelayState => 'http://example.com') + params = RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message", :RelayState => 'http://example.com') assert params['SAMLResponse'] assert params[:RelayState] assert params['Signature'] @@ -257,7 +257,7 @@ class SloLogoutresponseTest < Minitest::Test it "create a signature parameter with RSA_SHA256 /SHA256 and validate it" do settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA256 - params = OneLogin::RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message", :RelayState => 'http://example.com') + params = RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message", :RelayState => 'http://example.com') assert params['SAMLResponse'] assert params[:RelayState] assert params['Signature'] @@ -276,7 +276,7 @@ class SloLogoutresponseTest < Minitest::Test it "create a signature parameter with RSA_SHA384 / SHA384 and validate it" do settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA384 - params = OneLogin::RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message", :RelayState => 'http://example.com') + params = RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message", :RelayState => 'http://example.com') assert params['SAMLResponse'] assert params[:RelayState] assert params['Signature'] @@ -295,7 +295,7 @@ class SloLogoutresponseTest < Minitest::Test it "create a signature parameter with RSA_SHA512 / SHA512 and validate it" do settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA512 - params = OneLogin::RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message", :RelayState => 'http://example.com') + params = RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message", :RelayState => 'http://example.com') assert params['SAMLResponse'] assert params[:RelayState] assert params['Signature'] @@ -323,7 +323,7 @@ class SloLogoutresponseTest < Minitest::Test ] } - params = OneLogin::RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message", :RelayState => 'http://example.com') + params = RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message", :RelayState => 'http://example.com') assert params['SAMLResponse'] assert params[:RelayState] assert params['Signature'] @@ -341,8 +341,8 @@ class SloLogoutresponseTest < Minitest::Test it "raises error when no valid certs and :check_sp_cert_expiration is true" do settings.security[:check_sp_cert_expiration] = true - assert_raises(OneLogin::RubySaml::ValidationError, 'The SP certificate expired.') do - OneLogin::RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message", :RelayState => 'http://example.com') + assert_raises(RubySaml::ValidationError, 'The SP certificate expired.') do + RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message", :RelayState => 'http://example.com') end end end @@ -355,7 +355,7 @@ class SloLogoutresponseTest < Minitest::Test end it "doesn't sign through create_xml_document" do - unauth_res = OneLogin::RubySaml::SloLogoutresponse.new + unauth_res = RubySaml::SloLogoutresponse.new inflated = unauth_res.create_xml_document(settings).to_s refute_match %r[([a-zA-Z0-9/+=]+)], inflated @@ -364,7 +364,7 @@ class SloLogoutresponseTest < Minitest::Test end it "sign unsigned request" do - unauth_res = OneLogin::RubySaml::SloLogoutresponse.new + unauth_res = RubySaml::SloLogoutresponse.new unauth_res_doc = unauth_res.create_xml_document(settings) inflated = unauth_res_doc.to_s @@ -392,7 +392,7 @@ class SloLogoutresponseTest < Minitest::Test it "create a signature parameter with RSA_SHA1 and validate it" do settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA1 - params = OneLogin::RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message", :RelayState => 'http://example.com') + params = RubySaml::SloLogoutresponse.new.create_params(settings, logout_request.id, "Custom Logout Message", :RelayState => 'http://example.com') assert params['SAMLResponse'] assert params[:RelayState] assert params['Signature'] @@ -410,7 +410,7 @@ class SloLogoutresponseTest < Minitest::Test describe "#manipulate response_id" do it "be able to modify the response id" do - logoutresponse = OneLogin::RubySaml::SloLogoutresponse.new + logoutresponse = RubySaml::SloLogoutresponse.new response_id = logoutresponse.response_id assert_equal response_id, logoutresponse.uuid logoutresponse.uuid = "new_uuid" diff --git a/test/test_helper.rb b/test/test_helper.rb index 8fd82b762..d19d11b38 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -29,7 +29,7 @@ require 'onelogin/ruby-saml/logging' TEST_LOGGER = Logger.new(StringIO.new) -OneLogin::RubySaml::Logging.logger = TEST_LOGGER +RubySaml::Logging.logger = TEST_LOGGER class Minitest::Test def fixture(document, base64 = true) @@ -302,7 +302,7 @@ def ruby_saml_key_text # logoutresponse fixtures # def random_id - "_#{OneLogin::RubySaml::Utils.uuid}" + "_#{RubySaml::Utils.uuid}" end # diff --git a/test/utils_test.rb b/test/utils_test.rb index 0ab958abe..23360cb20 100644 --- a/test/utils_test.rb +++ b/test/utils_test.rb @@ -23,7 +23,7 @@ class UtilsTest < Minitest::Test def result(duration, reference = 0) Time.at( - OneLogin::RubySaml::Utils.parse_duration(duration, reference) + RubySaml::Utils.parse_duration(duration, reference) ).utc.iso8601(3) end @@ -45,45 +45,45 @@ def result(duration, reference = 0) it "returns empty string when the cert is an empty string" do cert = "" - assert_equal "", OneLogin::RubySaml::Utils.format_cert(cert) + assert_equal "", RubySaml::Utils.format_cert(cert) end it "returns nil when the cert is nil" do cert = nil - assert_nil OneLogin::RubySaml::Utils.format_cert(cert) + assert_nil RubySaml::Utils.format_cert(cert) end it "returns the certificate when it is valid" do - assert_equal formatted_certificate, OneLogin::RubySaml::Utils.format_cert(formatted_certificate) + assert_equal formatted_certificate, RubySaml::Utils.format_cert(formatted_certificate) end it "reformats the certificate when there are spaces and no line breaks" do invalid_certificate1 = read_certificate("invalid_certificate1") - assert_equal formatted_certificate, OneLogin::RubySaml::Utils.format_cert(invalid_certificate1) + assert_equal formatted_certificate, RubySaml::Utils.format_cert(invalid_certificate1) end it "reformats the certificate when there are spaces and no headers" do invalid_certificate2 = read_certificate("invalid_certificate2") - assert_equal formatted_certificate, OneLogin::RubySaml::Utils.format_cert(invalid_certificate2) + assert_equal formatted_certificate, RubySaml::Utils.format_cert(invalid_certificate2) end it "returns the cert when it's encoded" do encoded_certificate = read_certificate("certificate.der") - assert_equal encoded_certificate, OneLogin::RubySaml::Utils.format_cert(encoded_certificate) + assert_equal encoded_certificate, RubySaml::Utils.format_cert(encoded_certificate) end it "reformats the certificate when there line breaks and no headers" do invalid_certificate3 = read_certificate("invalid_certificate3") - assert_equal formatted_certificate, OneLogin::RubySaml::Utils.format_cert(invalid_certificate3) + assert_equal formatted_certificate, RubySaml::Utils.format_cert(invalid_certificate3) end it "returns the chained certificate when it is a valid chained certificate" do - assert_equal formatted_chained_certificate, OneLogin::RubySaml::Utils.format_cert(formatted_chained_certificate) + assert_equal formatted_chained_certificate, RubySaml::Utils.format_cert(formatted_chained_certificate) end it "reformats the chained certificate when there are spaces and no line breaks" do invalid_chained_certificate1 = read_certificate("invalid_chained_certificate1") - assert_equal formatted_chained_certificate, OneLogin::RubySaml::Utils.format_cert(invalid_chained_certificate1) + assert_equal formatted_chained_certificate, RubySaml::Utils.format_cert(invalid_chained_certificate1) end end @@ -94,31 +94,31 @@ def result(duration, reference = 0) it "returns empty string when the private key is an empty string" do private_key = "" - assert_equal "", OneLogin::RubySaml::Utils.format_private_key(private_key) + assert_equal "", RubySaml::Utils.format_private_key(private_key) end it "returns nil when the private key is nil" do private_key = nil - assert_nil OneLogin::RubySaml::Utils.format_private_key(private_key) + assert_nil RubySaml::Utils.format_private_key(private_key) end it "returns the private key when it is valid" do - assert_equal formatted_private_key, OneLogin::RubySaml::Utils.format_private_key(formatted_private_key) + assert_equal formatted_private_key, RubySaml::Utils.format_private_key(formatted_private_key) end it "reformats the private key when there are spaces and no line breaks" do invalid_private_key1 = read_certificate("invalid_private_key1") - assert_equal formatted_private_key, OneLogin::RubySaml::Utils.format_private_key(invalid_private_key1) + assert_equal formatted_private_key, RubySaml::Utils.format_private_key(invalid_private_key1) end it "reformats the private key when there are spaces and no headers" do invalid_private_key2 = read_certificate("invalid_private_key2") - assert_equal formatted_private_key, OneLogin::RubySaml::Utils.format_private_key(invalid_private_key2) + assert_equal formatted_private_key, RubySaml::Utils.format_private_key(invalid_private_key2) end it "reformats the private key when there line breaks and no headers" do invalid_private_key3 = read_certificate("invalid_private_key3") - assert_equal formatted_private_key, OneLogin::RubySaml::Utils.format_private_key(invalid_private_key3) + assert_equal formatted_private_key, RubySaml::Utils.format_private_key(invalid_private_key3) end describe "an RSA public key" do @@ -127,64 +127,64 @@ def result(duration, reference = 0) end it "returns the private key when it is valid" do - assert_equal formatted_rsa_private_key, OneLogin::RubySaml::Utils.format_private_key(formatted_rsa_private_key) + assert_equal formatted_rsa_private_key, RubySaml::Utils.format_private_key(formatted_rsa_private_key) end it "reformats the private key when there are spaces and no line breaks" do invalid_rsa_private_key1 = read_certificate("invalid_rsa_private_key1") - assert_equal formatted_rsa_private_key, OneLogin::RubySaml::Utils.format_private_key(invalid_rsa_private_key1) + assert_equal formatted_rsa_private_key, RubySaml::Utils.format_private_key(invalid_rsa_private_key1) end it "reformats the private key when there are spaces and no headers" do invalid_rsa_private_key2 = read_certificate("invalid_rsa_private_key2") - assert_equal formatted_private_key, OneLogin::RubySaml::Utils.format_private_key(invalid_rsa_private_key2) + assert_equal formatted_private_key, RubySaml::Utils.format_private_key(invalid_rsa_private_key2) end it "reformats the private key when there line breaks and no headers" do invalid_rsa_private_key3 = read_certificate("invalid_rsa_private_key3") - assert_equal formatted_private_key, OneLogin::RubySaml::Utils.format_private_key(invalid_rsa_private_key3) + assert_equal formatted_private_key, RubySaml::Utils.format_private_key(invalid_rsa_private_key3) end end end describe '.build_cert_object' do it 'returns a certificate object for valid certificate string' do - cert_object = OneLogin::RubySaml::Utils.build_cert_object(ruby_saml_cert_text) + cert_object = RubySaml::Utils.build_cert_object(ruby_saml_cert_text) assert_instance_of OpenSSL::X509::Certificate, cert_object end it 'returns nil for nil certificate string' do - assert_nil OneLogin::RubySaml::Utils.build_cert_object(nil) + assert_nil RubySaml::Utils.build_cert_object(nil) end it 'returns nil for empty certificate string' do - assert_nil OneLogin::RubySaml::Utils.build_cert_object('') + assert_nil RubySaml::Utils.build_cert_object('') end it 'raises error when given an invalid certificate string' do assert_raises OpenSSL::X509::CertificateError do - OneLogin::RubySaml::Utils.build_cert_object('Foobar') + RubySaml::Utils.build_cert_object('Foobar') end end end describe '.build_private_key_object' do it 'returns a private key object for valid private key string' do - private_key_object = OneLogin::RubySaml::Utils.build_private_key_object(ruby_saml_key_text) + private_key_object = RubySaml::Utils.build_private_key_object(ruby_saml_key_text) assert_instance_of OpenSSL::PKey::RSA, private_key_object end it 'returns nil for nil private key string' do - assert_nil OneLogin::RubySaml::Utils.build_private_key_object(nil) + assert_nil RubySaml::Utils.build_private_key_object(nil) end it 'returns nil for empty private key string' do - assert_nil OneLogin::RubySaml::Utils.build_private_key_object('') + assert_nil RubySaml::Utils.build_private_key_object('') end it 'raises error when given an invalid private key string' do assert_raises OpenSSL::PKey::RSAError do - OneLogin::RubySaml::Utils.build_private_key_object('Foobar') + RubySaml::Utils.build_private_key_object('Foobar') end end end @@ -196,7 +196,7 @@ def result(duration, reference = 0) params[:data] = "PHNhbWxwOkF1dGhuUmVxdWVzdCBEZXN0aW5hdGlvbj0naHR0cDovL2V4YW1wbGUuY29tP2ZpZWxkPXZhbHVlJyBJRD0nXzk4NmUxZDEwLWVhY2ItMDEzMi01MGRkLTAwOTBmNWRlZGQ3NycgSXNzdWVJbnN0YW50PScyMDE1LTA2LTAxVDIwOjM0OjU5WicgVmVyc2lvbj0nMi4wJyB4bWxuczpzYW1sPSd1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uJyB4bWxuczpzYW1scD0ndXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sJy8+" params[:relay_state] = "http://example.com" params[:sig_alg] = "http://www.w3.org/2000/09/xmldsig#rsa-sha1" - query_string = OneLogin::RubySaml::Utils.build_query(params) + query_string = RubySaml::Utils.build_query(params) assert_equal "SAMLRequest=PHNhbWxwOkF1dGhuUmVxdWVzdCBEZXN0aW5hdGlvbj0naHR0cDovL2V4YW1wbGUuY29tP2ZpZWxkPXZhbHVlJyBJRD0nXzk4NmUxZDEwLWVhY2ItMDEzMi01MGRkLTAwOTBmNWRlZGQ3NycgSXNzdWVJbnN0YW50PScyMDE1LTA2LTAxVDIwOjM0OjU5WicgVmVyc2lvbj0nMi4wJyB4bWxuczpzYW1sPSd1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uJyB4bWxuczpzYW1scD0ndXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sJy8%2B&RelayState=http%3A%2F%2Fexample.com&SigAlg=http%3A%2F%2Fwww.w3.org%2F2000%2F09%2Fxmldsig%23rsa-sha1", query_string end end @@ -211,12 +211,12 @@ def result(duration, reference = 0) it "returns true when the signature is valid" do @params[:signature] = "uWJm/T4gKLYEsVu1j/ZmjDeHp9zYPXPXWTXHFJZf2KKnWg57fUw3x2l6KTyRQ+Xjigb+sfYdGnnwmIz6KngXYRnh7nO6inspRLWOwkqQFy9iR9LDlMcfpXV/0g3oAxBxO6tX8MUHqR2R62SYZRGd1rxC9apg4vQiP97+atOI8t4=" - assert OneLogin::RubySaml::Utils.verify_signature(@params) + assert RubySaml::Utils.verify_signature(@params) end it "returns false when the signature is invalid" do @params[:signature] = "uWJm/InVaLiDsVu1j/ZmjDeHp9zYPXPXWTXHFJZf2KKnWg57fUw3x2l6KTyRQ+Xjigb+sfYdGnnwmIz6KngXYRnh7nO6inspRLWOwkqQFy9iR9LDlMcfpXV/0g3oAxBxO6tX8MUHqR2R62SYZRGd1rxC9apg4vQiP97+atOI8t4=" - assert !OneLogin::RubySaml::Utils.verify_signature(@params) + assert !RubySaml::Utils.verify_signature(@params) end end @@ -225,20 +225,20 @@ def result(duration, reference = 0) error_msg = "The status code of the Logout Response was not Success" status_code = "urn:oasis:names:tc:SAML:2.0:status:Requester" status_message = "The request could not be performed due to an error on the part of the requester." - status_error_msg = OneLogin::RubySaml::Utils.status_error_msg(error_msg, status_code, status_message) + status_error_msg = RubySaml::Utils.status_error_msg(error_msg, status_code, status_message) assert_equal "The status code of the Logout Response was not Success, was Requester -> The request could not be performed due to an error on the part of the requester.", status_error_msg end it "returns a error msg with status_code" do error_msg = "The status code of the Logout Response was not Success" status_code = "urn:oasis:names:tc:SAML:2.0:status:Requester" - status_error_msg = OneLogin::RubySaml::Utils.status_error_msg(error_msg, status_code) + status_error_msg = RubySaml::Utils.status_error_msg(error_msg, status_code) assert_equal "The status code of the Logout Response was not Success, was Requester", status_error_msg end it "returns a error msg" do error_msg = "The status code of the Logout Response was not Success" - status_error_msg = OneLogin::RubySaml::Utils.status_error_msg(error_msg) + status_error_msg = RubySaml::Utils.status_error_msg(error_msg) assert_equal "The status code of the Logout Response was not Success", status_error_msg end end @@ -247,11 +247,11 @@ def result(duration, reference = 0) describe ".uuid" do it "returns a uuid starting with an underscore" do - assert_match(/^_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/, OneLogin::RubySaml::Utils.uuid) + assert_match(/^_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/, RubySaml::Utils.uuid) end it "doesn't return the same value twice" do - refute_equal OneLogin::RubySaml::Utils.uuid, OneLogin::RubySaml::Utils.uuid + refute_equal RubySaml::Utils.uuid, RubySaml::Utils.uuid end end @@ -259,85 +259,85 @@ def result(duration, reference = 0) it 'matches two urls' do destination = 'http://www.example.com/test?var=stuff' settings = 'http://www.example.com/test?var=stuff' - assert OneLogin::RubySaml::Utils.uri_match?(destination, settings) + assert RubySaml::Utils.uri_match?(destination, settings) end it 'fails to match two urls' do destination = 'http://www.example.com/test?var=stuff' settings = 'http://www.example.com/othertest?var=stuff' - assert !OneLogin::RubySaml::Utils.uri_match?(destination, settings) + assert !RubySaml::Utils.uri_match?(destination, settings) end it "matches two URLs if the scheme case doesn't match" do destination = 'http://www.example.com/test?var=stuff' settings = 'HTTP://www.example.com/test?var=stuff' - assert OneLogin::RubySaml::Utils.uri_match?(destination, settings) + assert RubySaml::Utils.uri_match?(destination, settings) end it "matches two URLs if the host case doesn't match" do destination = 'http://www.EXAMPLE.com/test?var=stuff' settings = 'http://www.example.com/test?var=stuff' - assert OneLogin::RubySaml::Utils.uri_match?(destination, settings) + assert RubySaml::Utils.uri_match?(destination, settings) end it "fails to match two URLs if the path case doesn't match" do destination = 'http://www.example.com/TEST?var=stuff' settings = 'http://www.example.com/test?var=stuff' - assert !OneLogin::RubySaml::Utils.uri_match?(destination, settings) + assert !RubySaml::Utils.uri_match?(destination, settings) end it "fails to match two URLs if the query case doesn't match" do destination = 'http://www.example.com/test?var=stuff' settings = 'http://www.example.com/test?var=STUFF' - assert !OneLogin::RubySaml::Utils.uri_match?(destination, settings) + assert !RubySaml::Utils.uri_match?(destination, settings) end it 'matches two non urls' do destination = 'stuff' settings = 'stuff' - assert OneLogin::RubySaml::Utils.uri_match?(destination, settings) + assert RubySaml::Utils.uri_match?(destination, settings) end it "fails to match two non urls" do destination = 'stuff' settings = 'not stuff' - assert !OneLogin::RubySaml::Utils.uri_match?(destination, settings) + assert !RubySaml::Utils.uri_match?(destination, settings) end end describe '.element_text' do it 'returns the element text' do element = REXML::Document.new('element text').elements.first - assert_equal 'element text', OneLogin::RubySaml::Utils.element_text(element) + assert_equal 'element text', RubySaml::Utils.element_text(element) end it 'returns all segments of the element text' do element = REXML::Document.new('element text').elements.first - assert_equal 'element text', OneLogin::RubySaml::Utils.element_text(element) + assert_equal 'element text', RubySaml::Utils.element_text(element) end it 'returns normalized element text' do element = REXML::Document.new('element & text').elements.first - assert_equal 'element & text', OneLogin::RubySaml::Utils.element_text(element) + assert_equal 'element & text', RubySaml::Utils.element_text(element) end it 'returns the CDATA element text' do element = REXML::Document.new('').elements.first - assert_equal 'element & text', OneLogin::RubySaml::Utils.element_text(element) + assert_equal 'element & text', RubySaml::Utils.element_text(element) end it 'returns the element text with newlines and additional whitespace' do element = REXML::Document.new(" element \n text ").elements.first - assert_equal " element \n text ", OneLogin::RubySaml::Utils.element_text(element) + assert_equal " element \n text ", RubySaml::Utils.element_text(element) end it 'returns nil when element is nil' do - assert_nil OneLogin::RubySaml::Utils.element_text(nil) + assert_nil RubySaml::Utils.element_text(nil) end it 'returns empty string when element has no text' do element = REXML::Document.new('').elements.first - assert_equal '', OneLogin::RubySaml::Utils.element_text(element) + assert_equal '', RubySaml::Utils.element_text(element) end end end @@ -346,8 +346,8 @@ def result(duration, reference = 0) let(:private_key) { ruby_saml_key } let(:invalid_key1) { CertificateHelper.generate_key } let(:invalid_key2) { CertificateHelper.generate_key } - let(:settings) { OneLogin::RubySaml::Settings.new(:private_key => private_key.to_pem) } - let(:response) { OneLogin::RubySaml::Response.new(signed_message_encrypted_unsigned_assertion, :settings => settings) } + let(:settings) { RubySaml::Settings.new(:private_key => private_key.to_pem) } + let(:response) { RubySaml::Response.new(signed_message_encrypted_unsigned_assertion, :settings => settings) } let(:encrypted) do REXML::XPath.first( response.document, @@ -357,34 +357,34 @@ def result(duration, reference = 0) end it 'successfully decrypts with the first private key' do - assert_match /\Ab9xsAXLsynugg3Wc1CI3kpWku+0=") mod_document = XMLSecurity::SignedDocument.new(decoded_response) base64cert = mod_document.elements["//ds:X509Certificate"].text - exception = assert_raises(OneLogin::RubySaml::ValidationError) do + exception = assert_raises(RubySaml::ValidationError) do mod_document.validate_signature(base64cert, false) end assert_equal("Key validation error", exception.message) @@ -71,7 +71,7 @@ class XmlSecurityTest < Minitest::Test it "raise validation error when the X509Certificate is missing and no cert provided" do decoded_response.sub!(/.*<\/ds:X509Certificate>/, "") mod_document = XMLSecurity::SignedDocument.new(decoded_response) - exception = assert_raises(OneLogin::RubySaml::ValidationError) do + exception = assert_raises(RubySaml::ValidationError) do mod_document.validate_document("a fingerprint", false) # The fingerprint isn't relevant to this test end assert_equal("Certificate element missing in response (ds:X509Certificate) and not cert provided at settings", exception.message) @@ -134,7 +134,7 @@ class XmlSecurityTest < Minitest::Test end describe "Fingerprint Algorithms" do - let(:response_fingerprint_test) { OneLogin::RubySaml::Response.new(fixture(:adfs_response_sha1, false)) } + let(:response_fingerprint_test) { RubySaml::Response.new(fixture(:adfs_response_sha1, false)) } it "validate using SHA1" do sha1_fingerprint = "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72" @@ -213,7 +213,7 @@ class XmlSecurityTest < Minitest::Test it 'support inclusive canonicalization' do skip('test not yet implemented') - response = OneLogin::RubySaml::Response.new(fixture("tdnf_response.xml")) + response = RubySaml::Response.new(fixture("tdnf_response.xml")) response.stubs(:conditions).returns(nil) assert !response.is_valid? assert !response.is_valid? @@ -245,13 +245,13 @@ class XmlSecurityTest < Minitest::Test end it "sign an AuthNRequest" do - request = OneLogin::RubySaml::Authrequest.new.create_authentication_xml_doc(settings) + request = RubySaml::Authrequest.new.create_authentication_xml_doc(settings) request.sign_document(ruby_saml_key, ruby_saml_cert) # verify our signature signed_doc = XMLSecurity::SignedDocument.new(request.to_s) assert signed_doc.validate_document(ruby_saml_cert_fingerprint, false) - request2 = OneLogin::RubySaml::Authrequest.new.create_authentication_xml_doc(settings) + request2 = RubySaml::Authrequest.new.create_authentication_xml_doc(settings) request2.sign_document(ruby_saml_key, ruby_saml_cert_text) # verify our signature signed_doc2 = XMLSecurity::SignedDocument.new(request2.to_s) @@ -259,7 +259,7 @@ class XmlSecurityTest < Minitest::Test end it "sign an AuthNRequest with certificate as text" do - request = OneLogin::RubySaml::Authrequest.new.create_authentication_xml_doc(settings) + request = RubySaml::Authrequest.new.create_authentication_xml_doc(settings) request.sign_document(ruby_saml_key, ruby_saml_cert_text) # verify our signature @@ -268,13 +268,13 @@ class XmlSecurityTest < Minitest::Test end it "sign a LogoutRequest" do - logout_request = OneLogin::RubySaml::Logoutrequest.new.create_logout_request_xml_doc(settings) + logout_request = RubySaml::Logoutrequest.new.create_logout_request_xml_doc(settings) logout_request.sign_document(ruby_saml_key, ruby_saml_cert) # verify our signature signed_doc = XMLSecurity::SignedDocument.new(logout_request.to_s) assert signed_doc.validate_document(ruby_saml_cert_fingerprint, false) - logout_request2 = OneLogin::RubySaml::Logoutrequest.new.create_logout_request_xml_doc(settings) + logout_request2 = RubySaml::Logoutrequest.new.create_logout_request_xml_doc(settings) logout_request2.sign_document(ruby_saml_key, ruby_saml_cert_text) # verify our signature signed_doc2 = XMLSecurity::SignedDocument.new(logout_request2.to_s) @@ -283,13 +283,13 @@ class XmlSecurityTest < Minitest::Test end it "sign a LogoutResponse" do - logout_response = OneLogin::RubySaml::SloLogoutresponse.new.create_logout_response_xml_doc(settings, 'request_id_example', "Custom Logout Message") + logout_response = RubySaml::SloLogoutresponse.new.create_logout_response_xml_doc(settings, 'request_id_example', "Custom Logout Message") logout_response.sign_document(ruby_saml_key, ruby_saml_cert) # verify our signature signed_doc = XMLSecurity::SignedDocument.new(logout_response.to_s) assert signed_doc.validate_document(ruby_saml_cert_fingerprint, false) - logout_response2 = OneLogin::RubySaml::SloLogoutresponse.new.create_logout_response_xml_doc(settings, 'request_id_example', "Custom Logout Message") + logout_response2 = RubySaml::SloLogoutresponse.new.create_logout_response_xml_doc(settings, 'request_id_example', "Custom Logout Message") logout_response2.sign_document(ruby_saml_key, ruby_saml_cert_text) # verify our signature signed_doc2 = XMLSecurity::SignedDocument.new(logout_response2.to_s) @@ -299,10 +299,10 @@ class XmlSecurityTest < Minitest::Test end describe "StarfieldTMS" do - let (:response) { OneLogin::RubySaml::Response.new(fixture(:starfield_response)) } + let (:response) { RubySaml::Response.new(fixture(:starfield_response)) } before do - response.settings = OneLogin::RubySaml::Settings.new(:idp_cert_fingerprint => "8D:BA:53:8E:A3:B6:F9:F1:69:6C:BB:D9:D8:BD:41:B3:AC:4F:9D:4D") + response.settings = RubySaml::Settings.new(:idp_cert_fingerprint => "8D:BA:53:8E:A3:B6:F9:F1:69:6C:BB:D9:D8:BD:41:B3:AC:4F:9D:4D") end it "be able to validate a good response" do @@ -342,7 +342,7 @@ class XmlSecurityTest < Minitest::Test describe 'with valid document' do describe 'when response has signed message and assertion' do let(:document_data) { read_response('response_with_signed_message_and_assertion.xml') } - let(:document) { OneLogin::RubySaml::Response.new(document_data).document } + let(:document) { RubySaml::Response.new(document_data).document } let(:fingerprint) { '4b68c453c7d994aad9025c99d5efcf566287fe8d' } it 'is valid' do @@ -352,7 +352,7 @@ class XmlSecurityTest < Minitest::Test describe 'when response has signed assertion' do let(:document_data) { read_response('response_with_signed_assertion_3.xml') } - let(:document) { OneLogin::RubySaml::Response.new(document_data).document } + let(:document) { RubySaml::Response.new(document_data).document } let(:fingerprint) { '4b68c453c7d994aad9025c99d5efcf566287fe8d' } it 'is valid' do @@ -363,7 +363,7 @@ class XmlSecurityTest < Minitest::Test describe 'signature_wrapping_attack' do let(:document_data) { read_invalid_response("signature_wrapping_attack.xml.base64") } - let(:document) { OneLogin::RubySaml::Response.new(document_data).document } + let(:document) { RubySaml::Response.new(document_data).document } let(:fingerprint) { 'afe71c28ef740bc87425be13a2263d37971da1f9' } it 'is invalid' do @@ -373,7 +373,7 @@ class XmlSecurityTest < Minitest::Test describe 'signature wrapping attack - doubled SAML response body' do let(:document_data) { read_invalid_response("response_with_doubled_signed_assertion.xml") } - let(:document) { OneLogin::RubySaml::Response.new(document_data) } + let(:document) { RubySaml::Response.new(document_data) } let(:fingerprint) { '4b68c453c7d994aad9025c99d5efcf566287fe8d' } it 'is valid, but the unsigned information is ignored in favour of the signed information' do @@ -384,7 +384,7 @@ class XmlSecurityTest < Minitest::Test describe 'signature wrapping attack - concealed SAML response body' do let(:document_data) { read_invalid_response("response_with_concealed_signed_assertion.xml") } - let(:document) { OneLogin::RubySaml::Response.new(document_data) } + let(:document) { RubySaml::Response.new(document_data) } let(:fingerprint) { '4b68c453c7d994aad9025c99d5efcf566287fe8d' } it 'is valid, but fails to retrieve information' do @@ -396,17 +396,17 @@ class XmlSecurityTest < Minitest::Test describe '#validate_document_with_cert' do let(:document_data) { read_response('response_with_signed_message_and_assertion.xml') } - let(:document) { OneLogin::RubySaml::Response.new(document_data).document } + let(:document) { RubySaml::Response.new(document_data).document } let(:idp_cert) { OpenSSL::X509::Certificate.new(ruby_saml_cert_text) } let(:fingerprint) { '4b68c453c7d994aad9025c99d5efcf566287fe8d' } describe 'with invalid document ' do describe 'when certificate is invalid' do - let(:document) { OneLogin::RubySaml::Response.new(document_data).document } + let(:document) { RubySaml::Response.new(document_data).document } it 'is invalid' do wrong_document_data = document_data.sub(/.*<\/ds:X509Certificate>/, "invalid<\/ds:X509Certificate>") - wrong_document = OneLogin::RubySaml::Response.new(wrong_document_data).document + wrong_document = RubySaml::Response.new(wrong_document_data).document refute wrong_document.validate_document_with_cert(idp_cert), 'Document should be invalid' end end @@ -420,7 +420,7 @@ class XmlSecurityTest < Minitest::Test end describe 'when response has no cert but you have local cert' do - let(:document) { OneLogin::RubySaml::Response.new(response_document_valid_signed_without_x509certificate).document } + let(:document) { RubySaml::Response.new(response_document_valid_signed_without_x509certificate).document } let(:idp_cert) { OpenSSL::X509::Certificate.new(ruby_saml_cert_text) } it 'is valid' do @@ -454,7 +454,7 @@ class XmlSecurityTest < Minitest::Test let(:idp_cert) { OpenSSL::X509::Certificate.new(ruby_saml_cert_text2) } it 'is not valid' do - exception = assert_raises(OneLogin::RubySaml::ValidationError) do + exception = assert_raises(RubySaml::ValidationError) do document.validate_document_with_cert(idp_cert, false) end assert_equal("Certificate of the Signature element does not match provided certificate", exception.message)