require "xml_security" require "time" module Samlsso class Logoutresponse < SamlMessage # For API compability, this is mutable. attr_accessor :settings attr_reader :document attr_reader :response attr_reader :options # # In order to validate that the response matches a given request, append # the option: # :matches_request_id => REQUEST_ID # # It will validate that the logout response matches the ID of the request. # You can also do this yourself through the in_response_to accessor. # def initialize(response, settings = nil, options = {}) raise ArgumentError.new("Logoutresponse cannot be nil") if response.nil? self.settings = settings @options = options @response = decode_raw_saml(response) @document = XMLSecurity::SignedDocument.new(@response) end def validate! validate(false) end def validate(soft = true) return false unless valid_saml?(document, soft) && valid_state?(soft) valid_in_response_to?(soft) && valid_issuer?(soft) && success?(soft) end def success?(soft = true) unless status_code == "urn:oasis:names:tc:SAML:2.0:status:Success" return soft ? false : validation_error("Bad status code. Expected , but was: <#@status_code> ") end true end 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 def issuer @issuer ||= begin node = REXML::XPath.first(document, "/p:LogoutResponse/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION }) node ||= REXML::XPath.first(document, "/p:LogoutResponse/a:Assertion/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION }) node.nil? ? nil : node.text end end 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 private def valid_state?(soft = true) if response.empty? return soft ? false : validation_error("Blank response") end if settings.nil? return soft ? false : validation_error("No settings on response") end if settings.issuer.nil? return soft ? false : validation_error("No issuer in settings") end if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil? return soft ? false : validation_error("No fingerprint or certificate on settings") end true end def valid_in_response_to?(soft = true) return true unless self.options.has_key? :matches_request_id unless self.options[:matches_request_id] == in_response_to return soft ? false : validation_error("Response does not match the request ID, expected: <#{self.options[:matches_request_id]}>, but was: <#{in_response_to}>") end true end def valid_issuer?(soft = true) return true if self.settings.idp_entity_id.nil? or self.issuer.nil? unless URI.parse(self.issuer) == URI.parse(self.settings.idp_entity_id) return soft ? false : validation_error("Doesn't match the issuer, expected: <#{self.settings.issuer}>, but was: <#{issuer}>") end true end end end