require "xml_security" require "onelogin/ruby-saml/saml_message" require "time" # Only supports SAML 2.0 module OneLogin 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 @options = options @response = decode_raw_saml(response) @document = XMLSecurity::SignedDocument.new(@response) 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? unless status_code == "urn:oasis:names:tc:SAML:2.0:status:Success" return append_error("Bad status code. Expected <urn:oasis:names:tc:SAML:2.0:status:Success>, but was: <#@status_code>") end true 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, "a" => ASSERTION } ) node.nil? ? nil : node.attributes['InResponseTo'] end 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 } ) node.nil? ? nil : node.text 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, "a" => ASSERTION }) node.nil? ? nil : node.attributes["Value"] end end def status_message @status_message ||= begin node = REXML::XPath.first( document, "/p:LogoutResponse/p:Status/p:StatusMessage", { "p" => PROTOCOL, "a" => ASSERTION } ) node.text if node 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 = [ :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 private # 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? 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 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 true 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? return append_error("No settings on logout response") if settings.nil? return append_error("No issuer in settings of the logout response") if settings.issuer.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 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.has_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 # 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 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 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 unless !options.nil? return true unless options.has_key? :get_params return true unless options[:get_params].has_key? 'Signature' options[:raw_get_params] = OneLogin::RubySaml::Utils.prepare_raw_get_params(options[:raw_get_params], options[:get_params]) 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 if idp_cert.nil? && (idp_certs.nil? || idp_certs[:signing].empty?) return options.has_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'] ) if idp_certs.nil? || idp_certs[:signing].empty? valid = OneLogin::RubySaml::Utils.verify_signature( :cert => settings.get_idp_cert, :sig_alg => options[:get_params]['SigAlg'], :signature => options[:get_params]['Signature'], :query_string => query_string ) else valid = false idp_certs[:signing].each do |idp_cert| valid = OneLogin::RubySaml::Utils.verify_signature( :cert => idp_cert, :sig_alg => options[:get_params]['SigAlg'], :signature => options[:get_params]['Signature'], :query_string => query_string ) if valid break end end end unless valid error_msg = "Invalid Signature on Logout Response" return append_error(error_msg) end true end end end end