class Samlsso::Response

Constants

ASSERTION
DSIG
PROTOCOL

Attributes

decoded_document[R]
decoded_response[R]
decrypted_response[R]
document[R]
errors[RW]
options[R]
response[R]
settings[RW]

TODO: This should probably be ctor initialized too… WDYT?

Public Class Methods

new(response, options = {}) click to toggle source
# File lib/samlsso/response.rb, line 24
def initialize(response, options = {})
  @errors = []
  raise ArgumentError.new("Response cannot be nil") if response.nil?
  @options  = options
  @decoded_response = decode_raw_saml(response)
  @decrypted_response = decrypt_saml(@decoded_response, @options[:private_key_file_path])
  @response = @decrypted_response
  @document = XMLSecurity::SignedDocument.new(@response, @errors)
  @decoded_document = XMLSecurity::SignedDocument.new(@decoded_response, @errors)
end

Public Instance Methods

attributes() click to toggle source

Returns Samlsso::Attributes enumerable collection. All attributes can be iterated over attributes.each or returned as array by attributes.all

For backwards compatibility samlsso 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:

Samlsso::Attributes.single_value_compatibility = false

Now this will return an array:

attributes['name']
# File lib/samlsso/response.rb, line 73
def attributes
  @attr_statements ||= begin
    attributes = Attributes.new

    stmt_element = xpath_first_from_signed_assertion('/a:AttributeStatement')
    return attributes if stmt_element.nil?

    stmt_element.elements.each do |attr_element|
      name  = attr_element.attributes["Name"] ? attr_element.attributes["Name"] : attr_element.attributes["name"]
      values = attr_element.elements.collect{|e|
        # 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.
        ["true", "1"].include?(e.attributes['xsi:nil']) ? nil : e.text.to_s
      }

      attributes.add(name, values)
    end

    attributes
  end
end
conditions() click to toggle source

Conditions (if any) for the assertion to run

# File lib/samlsso/response.rb, line 119
def conditions
  @conditions ||= xpath_first_from_signed_assertion('/a:Conditions')
end
is_valid?() click to toggle source
# File lib/samlsso/response.rb, line 35
def is_valid?
  validate
end
issuer() click to toggle source
# File lib/samlsso/response.rb, line 131
def issuer
  @issuer ||= begin
    node = REXML::XPath.first(document, "/p:Response/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
    node ||= xpath_first_from_signed_assertion('/a:Issuer')
    node.nil? ? nil : node.text
  end
end
name_id() click to toggle source

The value of the user identifier as designated by the initialization request response

# File lib/samlsso/response.rb, line 48
def name_id
  @name_id ||= begin
    node = xpath_first_from_signed_assertion('/a:Subject/a:NameID')
    node.nil? ? nil : node.text
  end
end
not_before() click to toggle source
# File lib/samlsso/response.rb, line 123
def not_before
  @not_before ||= parse_time(conditions, "NotBefore")
end
not_on_or_after() click to toggle source
# File lib/samlsso/response.rb, line 127
def not_on_or_after
  @not_on_or_after ||= parse_time(conditions, "NotOnOrAfter")
end
session_expires_at() click to toggle source

When this user session should expire at latest

# File lib/samlsso/response.rb, line 96
def session_expires_at
  @expires_at ||= begin
    node = xpath_first_from_signed_assertion('/a:AuthnStatement')
    parse_time(node, "SessionNotOnOrAfter")
  end
end
sessionindex() click to toggle source
# File lib/samlsso/response.rb, line 55
def sessionindex
  @sessionindex ||= begin
    node = xpath_first_from_signed_assertion('/a:AuthnStatement')
    node.nil? ? nil : (node.attributes['SessionIndex'] ? node.attributes['SessionIndex'] : node.attributes['sessionindex'])
  end
end
status_message() click to toggle source
# File lib/samlsso/response.rb, line 111
def status_message
  @status_message ||= begin
    node = REXML::XPath.first(document, "/p:Response/p:Status/p:StatusMessage", { "p" => PROTOCOL, "a" => ASSERTION })
    node.text if node
  end
end
success?() click to toggle source

Checks the status of the response for a “Success” code

# File lib/samlsso/response.rb, line 104
def success?
  @status_code ||= begin
    node = REXML::XPath.first(document, "/p:Response/p:Status/p:StatusCode", { "p" => PROTOCOL, "a" => ASSERTION })
    node.attributes["Value"] == "urn:oasis:names:tc:SAML:2.0:status:Success" || node.attributes["value"] == "urn:oasis:names:tc:SAML:2.0:status:Success"
  end
end
validate!() click to toggle source
# File lib/samlsso/response.rb, line 39
def validate!
  validate(false)
end

Private Instance Methods

get_fingerprint() click to toggle source
# File lib/samlsso/response.rb, line 227
def get_fingerprint
  if settings.idp_cert
    cert = OpenSSL::X509::Certificate.new(settings.idp_cert)
    Digest::SHA1.hexdigest(cert.to_der).upcase.scan(/../).join(":")
  else
    settings.idp_cert_fingerprint
  end
end
parse_time(node, attribute) click to toggle source
# File lib/samlsso/response.rb, line 264
def parse_time(node, attribute)
  if node
    attrs = node.attributes[attribute] ? node.attributes[attribute] : node.attributes[attribute.downcase]
    Time.parse(attrs) if attrs
  end
end
validate(soft = true) click to toggle source
# File lib/samlsso/response.rb, line 141
def validate(soft = true)
  valid_saml?(decoded_document, soft)      &&
  validate_response_state(soft) &&
  validate_conditions(soft)     &&
  validate_issuer(soft)         &&
  decoded_document.validate_document(get_fingerprint, soft) &&
  validate_success_status(soft)
end
validate_conditions(soft = true) click to toggle source
# File lib/samlsso/response.rb, line 236
def validate_conditions(soft = true)
  return true if conditions.nil?
  return true if options[:skip_conditions]

  now = Time.now.utc

  if not_before && (now + (options[:allowed_clock_drift] || 0)) < not_before
    @errors << "Current time is earlier than NotBefore condition #{(now + (options[:allowed_clock_drift] || 0))} < #{not_before})"
    return soft ? false : validation_error("Current time is earlier than NotBefore condition")
  end

  if not_on_or_after && now >= not_on_or_after
    @errors << "Current time is on or after NotOnOrAfter condition (#{now} >= #{not_on_or_after})"
    return soft ? false : validation_error("Current time is on or after NotOnOrAfter condition")
  end

  true
end
validate_issuer(soft = true) click to toggle source
# File lib/samlsso/response.rb, line 255
def validate_issuer(soft = true)
  return true if settings.idp_entity_id.nil?

  unless URI.parse(issuer) == URI.parse(settings.idp_entity_id)
    return soft ? false : validation_error("Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>")
  end
  true
end
validate_response_state(soft = true) click to toggle source
# File lib/samlsso/response.rb, line 175
def validate_response_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.idp_cert_fingerprint.nil? && settings.idp_cert.nil?
    return soft ? false : validation_error("No fingerprint or certificate on settings")
  end

  true
end
validate_structure(soft = true) click to toggle source
# File lib/samlsso/response.rb, line 158
def validate_structure(soft = true)
  return true #temp
  xml = Nokogiri::XML(self.document.to_s)

  SamlMessage.schema.validate(xml).map do |error|
    if soft
      @errors << "Schema validation failed"
      break false
    else
      error_message = [error.message, xml.to_s].join("\n\n")

      @errors << error_message
      validation_error(error_message)
    end
  end
end
validate_success_status(soft = true) click to toggle source
# File lib/samlsso/response.rb, line 150
def validate_success_status(soft = true)
  if success?
    true
  else
    soft ? false : validation_error(status_message)
  end
end
xpath_first_from_signed_assertion(subelt=nil) click to toggle source
# File lib/samlsso/response.rb, line 191
def xpath_first_from_signed_assertion(subelt=nil)
  id_str = ""
  id_str = "[@ID=$id]" unless document.signed_element_id.blank?
  node = REXML::XPath.first(
      document,
      "/p:Response/a:Assertion#{id_str}#{subelt}",
      { "p" => PROTOCOL, "a" => ASSERTION },
      { 'id' => document.signed_element_id }
  )
  node ||= REXML::XPath.first(
      document,
      "/p:Response#{id_str}/a:Assertion#{subelt}",
      { "p" => PROTOCOL, "a" => ASSERTION },
      { 'id' => document.signed_element_id }
  )
  node ||= REXML::XPath.first(
      document,
      "/p:Response/a:assertion#{id_str}#{subelt}",
      { "p" => PROTOCOL, "a" => ASSERTION },
      { 'id' => document.signed_element_id }
  )
  node ||= REXML::XPath.first(
      document,
      "/p:Response#{id_str}/a:assertion#{subelt}",
      { "p" => PROTOCOL, "a" => ASSERTION },
      { 'id' => document.signed_element_id }
  ) 
  node ||= REXML::XPath.first(
      document,
      "/p:Response#{id_str}/a:assertion#{subelt.downcase}",
      { "p" => PROTOCOL, "a" => ASSERTION },
      { 'id' => document.signed_element_id }
  ) 
  node
end