Skip to content

Commit

Permalink
Added support for raising an exception when a condition fails validat…
Browse files Browse the repository at this point in the history
…ion.

Added option to skip conditions check back. Test for validate! method.
  • Loading branch information
morten committed Jun 23, 2011
1 parent 02a9d92 commit e1d7353
Show file tree
Hide file tree
Showing 7 changed files with 200 additions and 148 deletions.
1 change: 1 addition & 0 deletions lib/onelogin/saml.rb
@@ -1,4 +1,5 @@
require 'onelogin/saml/authrequest'
require 'onelogin/saml/response'
require 'onelogin/saml/settings'
require 'onelogin/saml/validation_error'

70 changes: 51 additions & 19 deletions lib/onelogin/saml/response.rb
Expand Up @@ -2,26 +2,27 @@
require "time"

module Onelogin::Saml

class Response
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#"

attr_accessor :response, :document, :logger, :settings, :original
attr_accessor :options, :response, :document, :settings

def initialize(response)
def initialize(response, options = {})
raise ArgumentError.new("Response cannot be nil") if response.nil?
self.options = options
self.response = response
self.document = XMLSecurity::SignedDocument.new(Base64.decode64(response))
end

def is_valid?
return false if response.empty?
return false if settings.nil?
return false if settings.idp_cert_fingerprint.nil?
return false if !check_conditions
validate(soft = true)
end

document.validate(settings.idp_cert_fingerprint, logger)
def validate!
validate(soft = false)
end

# The value of the user identifier as designated by the initialization request response
Expand All @@ -32,18 +33,6 @@ def name_id
end
end

def check_conditions
return true if conditions.nil?

not_before = parse_time(conditions, "NotBefore")
return false if not_before && Time.now.utc < not_before

not_on_or_after = parse_time(conditions, "NotOnOrAfter")
return false if not_on_or_after && Time.now.utc >= not_on_or_after

true
end

# A hash of alle the attributes with the response. Assuming there is only one value for each key
def attributes
@attr_statements ||= begin
Expand Down Expand Up @@ -84,6 +73,49 @@ def conditions

private

def validation_error(message)
raise ValidationError.new(message)
end

def validate(soft = true)
validate_response_state(soft) && validate_conditions(soft) && document.validate(settings.idp_cert_fingerprint)
end

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?
return soft ? false : validation_error("No fingerpring on settings")
end

true
end

def validate_conditions(soft = true)
return true if conditions.nil?
return true if options[:skip_conditions]

if not_before = parse_time(conditions, "NotBefore")
if Time.now.utc < not_before
return soft ? false : validation_error("Current time is earlier than NotBefore condition")
end
end

if not_on_or_after = parse_time(conditions, "NotOnOrAfter")
if Time.now.utc >= not_on_or_after
return soft ? false : validation_error("Current time is on or after NotOnOrAfter condition")
end
end

true
end

def parse_time(node, attribute)
if node && node.attributes[attribute]
Time.parse(node.attributes[attribute])
Expand Down
7 changes: 7 additions & 0 deletions lib/onelogin/saml/validation_error.rb
@@ -0,0 +1,7 @@
module Onelogin
module Saml
class ValidationError < Exception
end
end
end

29 changes: 16 additions & 13 deletions lib/xml_security.rb
Expand Up @@ -28,11 +28,12 @@
require "openssl"
require "xmlcanonicalizer"
require "digest/sha1"
require "onelogin/saml/validation_error"

module XMLSecurity

class SignedDocument < REXML::Document
DSIG = "http://www.w3.org/2000/09/xmldsig#"
DSIG = "http://www.w3.org/2000/09/xmldsig#"

attr_accessor :signed_element_id

Expand All @@ -41,41 +42,41 @@ def initialize(response)
extract_signed_element_id
end

def validate (idp_cert_fingerprint, logger = nil)
def validate(idp_cert_fingerprint, soft = true)
# get cert from response
base64_cert = self.elements["//ds:X509Certificate"].text
cert_text = Base64.decode64(base64_cert)
cert = OpenSSL::X509::Certificate.new(cert_text)

# check cert matches registered idp cert
fingerprint = Digest::SHA1.hexdigest(cert.to_der)
valid_flag = fingerprint == idp_cert_fingerprint.gsub(/[^a-zA-Z0-9]/,"").downcase

return valid_flag if !valid_flag
if fingerprint != idp_cert_fingerprint.gsub(/[^a-zA-Z0-9]/,"").downcase
return soft ? false : (raise Onelogin::Saml::ValidationError.new("Fingerprint mismatch"))
end

validate_doc(base64_cert, logger)
validate_doc(base64_cert)
end

def validate_doc(base64_cert, logger)
def validate_doc(base64_cert, soft = true)
# validate references

# remove signature node
sig_element = REXML::XPath.first(self, "//ds:Signature", {"ds"=>"http://www.w3.org/2000/09/xmldsig#"})
sig_element.remove

#check digests
REXML::XPath.each(sig_element, "//ds:Reference", {"ds"=>"http://www.w3.org/2000/09/xmldsig#"}) do | ref |

REXML::XPath.each(sig_element, "//ds:Reference", {"ds"=>"http://www.w3.org/2000/09/xmldsig#"}) do |ref|
uri = ref.attributes.get_attribute("URI").value
hashed_element = REXML::XPath.first(self, "//[@ID='#{uri[1,uri.size]}']")
canoner = XML::Util::XmlCanonicalizer.new(false, true)
canon_hashed_element = canoner.canonicalize(hashed_element)
hash = Base64.encode64(Digest::SHA1.digest(canon_hashed_element)).chomp
digest_value = REXML::XPath.first(ref, "//ds:DigestValue", {"ds"=>"http://www.w3.org/2000/09/xmldsig#"}).text

valid_flag = hash == digest_value

return valid_flag if !valid_flag
if hash != digest_value
return soft ? false : (raise Onelogin::Saml::ValidationError.new("Digest mismatch"))
end
end

# verify signature
Expand All @@ -90,9 +91,11 @@ def validate_doc(base64_cert, logger)
cert_text = Base64.decode64(base64_cert)
cert = OpenSSL::X509::Certificate.new(cert_text)

valid_flag = cert.public_key.verify(OpenSSL::Digest::SHA1.new, signature, canon_string)
if !cert.public_key.verify(OpenSSL::Digest::SHA1.new, signature, canon_string)
return soft ? false : (raise ValidationError.new("Key validation error"))
end

return valid_flag
return true
end

private
Expand Down
124 changes: 124 additions & 0 deletions test/response_test.rb
@@ -0,0 +1,124 @@
require File.expand_path(File.join(File.dirname(__FILE__), "test_helper"))

class RubySamlTest < Test::Unit::TestCase

context "Response" do
should "raise an exception when response is initialized with nil" do
assert_raises(ArgumentError) { Onelogin::Saml::Response.new(nil) }
end

should "adapt namespace" do
response = Onelogin::Saml::Response.new(response_document)
assert !response.name_id.nil?
response = Onelogin::Saml::Response.new(response_document_2)
assert !response.name_id.nil?
response = Onelogin::Saml::Response.new(response_document_3)
assert !response.name_id.nil?
end

context "#validate!" do
should "raise when encountering a condition that prevents the document from being valid" do
response = Onelogin::Saml::Response.new(response_document)
assert_raise(Onelogin::Saml::ValidationError) do
response.validate!
end
end
end

context "#is_valid?" do
should "return false when response is initialized with blank data" do
response = Onelogin::Saml::Response.new('')
assert !response.is_valid?
end

should "return false if settings have not been set" do
response = Onelogin::Saml::Response.new(response_document)
assert !response.is_valid?
end

should "return true when the response is initialized with valid data" do
response = Onelogin::Saml::Response.new(response_document_4)
response.stubs(:conditions).returns(nil)
assert !response.is_valid?
settings = Onelogin::Saml::Settings.new
assert !response.is_valid?
response.settings = settings
assert !response.is_valid?
settings.idp_cert_fingerprint = signature_fingerprint_1
assert response.is_valid?
end

should "not allow signature wrapping attack" do
response = Onelogin::Saml::Response.new(response_document_4)
response.stubs(:conditions).returns(nil)
settings = Onelogin::Saml::Settings.new
settings.idp_cert_fingerprint = signature_fingerprint_1
response.settings = settings
assert response.is_valid?
assert response.name_id == "test@onelogin.com"
end
end

context "#name_id" do
should "extract the value of the name id element" do
response = Onelogin::Saml::Response.new(response_document)
assert_equal "support@onelogin.com", response.name_id

response = Onelogin::Saml::Response.new(response_document_3)
assert_equal "someone@example.com", response.name_id
end
end

context "#check_conditions" do
should "check time conditions" do
response = Onelogin::Saml::Response.new(response_document)
assert !response.send(:validate_conditions, true)
response = Onelogin::Saml::Response.new(response_document_6)
assert response.send(:validate_conditions, true)
time = Time.parse("2011-06-14T18:25:01.516Z")
Time.stubs(:now).returns(time)
response = Onelogin::Saml::Response.new(response_document_5)
assert response.send(:validate_conditions, true)
end
end

context "#attributes" do
should "extract the first attribute in a hash accessed via its symbol" do
response = Onelogin::Saml::Response.new(response_document)
assert_equal "demo", response.attributes[:uid]
end

should "extract the first attribute in a hash accessed via its name" do
response = Onelogin::Saml::Response.new(response_document)
assert_equal "demo", response.attributes["uid"]
end

should "extract all attributes" do
response = Onelogin::Saml::Response.new(response_document)
assert_equal "demo", response.attributes[:uid]
assert_equal "value", response.attributes[:another_value]
end

should "work for implicit namespaces" do
response = Onelogin::Saml::Response.new(response_document_3)
assert_equal "someone@example.com", response.attributes["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"]
end

should "not raise on responses without attributes" do
response = Onelogin::Saml::Response.new(response_document_4)
assert_equal Hash.new, response.attributes
end
end

context "#session_expires_at" do
should "extract the value of the SessionNotOnOrAfter attribute" do
response = Onelogin::Saml::Response.new(response_document)
assert response.session_expires_at.is_a?(Time)

response = Onelogin::Saml::Response.new(response_document_2)
assert response.session_expires_at.nil?
end
end
end

end

0 comments on commit e1d7353

Please sign in to comment.