class Maestrano::Saml::Response

Constants

ASSERTION
DSIG
PROTOCOL

Attributes

document[R]
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/maestrano/saml/response.rb, line 21
def initialize(response, options = {})
  raise ArgumentError.new("Response cannot be nil") if response.nil?
  @options  = options
  @response = (response =~ /^</) ? response : Base64.decode64(response)
  @document = Maestrano::XMLSecurity::SignedDocument.new(@response)
  @settings = Maestrano::SSO[self.class.preset].saml_settings
end

Public Instance Methods

attributes() click to toggle source

A hash of all the attributes with the response. Multiple values will be returned in the AttributeValue#values array in reverse order, when compared to XML

# File lib/maestrano/saml/response.rb, line 55
def attributes
  @attr_statements ||= begin
    result = {}

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

    stmt_element.elements.each do |attr_element|
      name  = attr_element.attributes["Name"]
      values = attr_element.elements.collect(&:text)

      # Set up a string-like wrapper for the values array
      attr_value = AttributeValue.new(values.first, values.reverse)
      # Merge values if the Attribute has already been seen
      if result[name]
        attr_value.values += result[name].values
      end

      result[name] = attr_value
    end

    result.keys.each do |key|
      result[key.intern] = result[key]
    end

    result
  end
end
conditions() click to toggle source

Conditions (if any) for the assertion to run

# File lib/maestrano/saml/response.rb, line 101
def conditions
  @conditions ||= xpath_first_from_signed_assertion('/a:Conditions')
end
is_valid?() click to toggle source
# File lib/maestrano/saml/response.rb, line 29
def is_valid?
  validate
end
issuer() click to toggle source
# File lib/maestrano/saml/response.rb, line 113
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/maestrano/saml/response.rb, line 38
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/maestrano/saml/response.rb, line 105
def not_before
  @not_before ||= parse_time(conditions, "NotBefore")
end
not_on_or_after() click to toggle source
# File lib/maestrano/saml/response.rb, line 109
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/maestrano/saml/response.rb, line 85
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/maestrano/saml/response.rb, line 45
def sessionindex
  @sessionindex ||= begin
    node = xpath_first_from_signed_assertion('/a:AuthnStatement')
    node.nil? ? nil : node.attributes['SessionIndex']
  end
end
success?() click to toggle source

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

# File lib/maestrano/saml/response.rb, line 93
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"
  end
end
validate!() click to toggle source
# File lib/maestrano/saml/response.rb, line 33
def validate!
  validate(false)
end

Private Instance Methods

get_fingerprint() click to toggle source
# File lib/maestrano/saml/response.rb, line 169
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/maestrano/saml/response.rb, line 195
def parse_time(node, attribute)
  if node && node.attributes[attribute]
    Time.parse(node.attributes[attribute])
  end
end
validate(soft = true) click to toggle source
# File lib/maestrano/saml/response.rb, line 127
def validate(soft = true)
  validate_structure(soft)      &&
  validate_response_state(soft) &&
  validate_conditions(soft)     &&
  document.validate_document(get_fingerprint, soft) &&
  success?
end
validate_conditions(soft = true) click to toggle source
# File lib/maestrano/saml/response.rb, line 178
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
    return soft ? false : validation_error("Current time is earlier than NotBefore condition")
  end

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

  true
end
validate_response_state(soft = true) click to toggle source
# File lib/maestrano/saml/response.rb, line 147
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/maestrano/saml/response.rb, line 135
def validate_structure(soft = true)
  Dir.chdir(File.expand_path(File.join(File.dirname(__FILE__), 'schemas'))) do
    @schema = Nokogiri::XML::Schema(IO.read('saml20protocol_schema.xsd'))
    @xml = Nokogiri::XML(self.document.to_s)
  end
  if soft
    @schema.validate(@xml).map{ return false }
  else
    @schema.validate(@xml).map{ |error| validation_error("#{error.message}\n\n#{@xml.to_s}") }
  end
end
validation_error(message) click to toggle source
# File lib/maestrano/saml/response.rb, line 123
def validation_error(message)
  raise ValidationError.new(message)
end
xpath_first_from_signed_assertion(subelt=nil) click to toggle source
# File lib/maestrano/saml/response.rb, line 163
def xpath_first_from_signed_assertion(subelt=nil)
  node = REXML::XPath.first(document, "/p:Response/a:Assertion[@ID='#{document.signed_element_id}']#{subelt}", { "p" => PROTOCOL, "a" => ASSERTION })
  node ||= REXML::XPath.first(document, "/p:Response[@ID='#{document.signed_element_id}']/a:Assertion#{subelt}", { "p" => PROTOCOL, "a" => ASSERTION })
  node
end