Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add validators and refactor soft parameter (Splited from 197) #235

Merged
merged 5 commits into from
May 26, 2015
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ In the above there are a few assumptions in place, one being that the response.n
def saml_settings
settings = OneLogin::RubySaml::Settings.new

settings.assertion_consumer_service_url = "http://#{request.host}/saml/finalize"
settings.assertion_consumer_service_url = "http://#{request.host}/saml/consume"
settings.issuer = "http://#{request.host}/saml/metadata"
settings.idp_sso_target_url = "https://app.onelogin.com/saml/metadata/#{OneLoginAppId}"
settings.idp_entity_id = "https://app.onelogin.com/saml/metadata/#{OneLoginAppId}"
Expand Down
71 changes: 33 additions & 38 deletions lib/onelogin/ruby-saml/logoutresponse.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ class Logoutresponse < SamlMessage
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
Expand All @@ -31,7 +33,13 @@ class Logoutresponse < SamlMessage
def initialize(response, settings = nil, options = {})
@errors = []
raise ArgumentError.new("Logoutresponse cannot be nil") if response.nil?
self.settings = settings
@settings = settings

if settings.nil? || settings.soft.nil?
@soft = true
else
@soft = settings.soft
end

@options = options
@response = decode_raw_saml(response)
Expand All @@ -40,7 +48,7 @@ def initialize(response, settings = nil, options = {})

# Append the cause to the errors array, and based on the value of soft, return false or raise
# an exception
def append_error(soft, error_msg)
def append_error(error_msg)
@errors << error_msg
return soft ? false : validation_error(error_msg)
end
Expand All @@ -50,36 +58,27 @@ def reset_errors!
@errors = []
end

# Hard aux function to validate the Logout Response (soft = false)
# @return [Boolean] TRUE if the SAML Response is valid
# @raise [ValidationError] If validation fails
#
def validate!
validate(false)
end

# Aux function to validate the Logout Response
# @return [Boolean] TRUE if the SAML Response is valid
# @raise [ValidationError] if soft == false and validation fails
#
def validate(soft = true)
def validate
reset_errors!

validate_structure(soft) &&
valid_state?(soft) &&
valid_in_response_to?(soft) &&
valid_issuer?(soft) &&
success?(soft)
validate_structure &&
valid_state? &&
valid_in_response_to? &&
valid_issuer? &&
success?
end

# Checks if the Status has the "Success" code
# @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the logout response is invalid or not)
# @return [Boolean] True if the StatusCode is Sucess
# @raise [ValidationError] if soft == false and validation fails
#
def success?(soft = true)
def success?
unless status_code == "urn:oasis:names:tc:SAML:2.0:status:Success"
return append_error(soft, "Bad status code. Expected <urn:oasis:names:tc:SAML:2.0:status:Success>, but was: <#@status_code> ")
return append_error("Bad status code. Expected <urn:oasis:names:tc:SAML:2.0:status:Success>, but was: <#@status_code> ")
end
true
end
Expand All @@ -98,7 +97,6 @@ def in_response_to
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
Expand All @@ -115,33 +113,31 @@ def status_code
private

# Validates the Logout Response against the specified schema.
# @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the logout response is invalid or not)
# @return [Boolean] True if the XML is valid, otherwise False if soft=True
# @raise [ValidationError] if soft == false and validation fails
#
def validate_structure(soft = true)
def validate_structure
unless valid_saml?(document, soft)
return append_error(soft, "Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd")
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
# @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the logout response is invalid or not)
# @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?(soft = true)
return append_error(soft, "Blank response") if response.empty?
def valid_state?
return append_error("Blank logout response") if response.empty?

return append_error(soft, "No settings on response") if settings.nil?
return append_error("No settings on logout response") if settings.nil?

return append_error(soft, "No issuer in settings") if settings.issuer.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?
return append_error(soft, "No fingerprint or certificate on settings")
return append_error("No fingerprint or certificate on settings of the logout response")
end

true
Expand All @@ -152,26 +148,25 @@ def valid_state?(soft = true)
# @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?(soft = true)
return true unless self.options.has_key? :matches_request_id
def valid_in_response_to?
return true unless options.has_key? :matches_request_id

unless self.options[:matches_request_id] == in_response_to
return append_error(soft, "Response does not match the request ID, expected: <#{self.options[:matches_request_id]}>, but was: <#{in_response_to}>")
unless options[:matches_request_id] == in_response_to
return append_error("Response does not match the request ID, expected: <#{options[:matches_request_id]}>, but was: <#{in_response_to}>")
end

true
end

# Validates the Issuer of the Logout Response
# @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the logout response is invalid or not)
# @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?(soft = true)
return true if self.settings.idp_entity_id.nil? or self.issuer.nil?
def valid_issuer?
return true if settings.idp_entity_id.nil? || issuer.nil?

unless URI.parse(self.issuer) == URI.parse(self.settings.idp_entity_id)
append_error(soft, "Doesn't match the issuer, expected: <#{self.settings.issuer}>, but was: <#{issuer}>")
unless URI.parse(issuer) == URI.parse(settings.idp_entity_id)
append_error("Doesn't match the issuer, expected: <#{settings.issuer}>, but was: <#{issuer}>")
end
true
end
Expand Down
71 changes: 34 additions & 37 deletions lib/onelogin/ruby-saml/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,39 @@ class Response < SamlMessage
# Array with the causes [Array of strings]
attr_accessor :errors

attr_reader :options
attr_reader :response
attr_reader :document
attr_reader :response
attr_reader :options

attr_accessor :soft

# Constructs the SAML Response. A Response Object that is an extension of the SamlMessage class.
# @param response [String] A UUEncoded SAML response from the IdP.
# @param options [Hash] Some options for the response validation process like skip the conditions validation
# @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object
# Or some options for the response validation process like skip the conditions validation
# with the :skip_conditions, or allow a clock_drift when checking dates with :allowed_clock_drift
#
def initialize(response, options = {})
@errors = []

raise ArgumentError.new("Response cannot be nil") if response.nil?
@options = options

@soft = true
if !options.empty? && !options[:settings].nil?
@settings = options[:settings]
if !options[:settings].soft.nil?
@soft = options[:settings].soft
end
end

@response = decode_raw_saml(response)
@document = XMLSecurity::SignedDocument.new(@response, @errors)
end

# Append the cause to the errors array, and based on the value of soft, return false or raise
# an exception
def append_error(soft, error_msg)
def append_error(error_msg)
@errors << error_msg
return soft ? false : validation_error(error_msg)
end
Expand All @@ -60,16 +72,6 @@ def is_valid?
validate
end

# Hard aux function to validate the SAML Response (soft = false)
# @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the response is invalid or not)
# @param request_id [String|nil] request_id The ID of the AuthNRequest sent by this SP to the IdP (if was sent any)
# @return [Boolean] TRUE if the SAML Response is valid
# @raise [ValidationError] if soft == false and validation fails
#
def validate!
validate(false)
end

# @return [String] the NameID provided by the SAML response from the IdP.
#
def name_id
Expand Down Expand Up @@ -194,40 +196,37 @@ def issuer
private

# Validates the SAML Response (calls several validation methods)
# @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the response is invalid or not)
# @return [Boolean] True if the SAML Response is valid, otherwise False if soft=True
# @raise [ValidationError] if soft == false and validation fails
#
def validate(soft = true)
def validate
reset_errors!

validate_response_state(soft) &&
validate_structure(soft) &&
validate_conditions(soft) &&
validate_issuer(soft) &&
validate_response_state &&
validate_structure &&
validate_conditions &&
validate_issuer &&
document.validate_document(settings.get_fingerprint, soft, :fingerprint_alg => settings.idp_cert_fingerprint_algorithm) &&
validate_success_status(soft)
validate_success_status
end

# Validates the Status of the SAML Response
# @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the response is invalid or not)
# @return [Boolean] True if the SAML Response contains a Success code, otherwise False if soft == false
# @raise [ValidationError] if soft == false and validation fails
#
def validate_success_status(soft = true)
def validate_success_status
return true if success?

return append_error(soft, status_message)
return append_error(status_message)
end

# Validates the SAML Response against the specified schema.
# @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the response is invalid or not)
# @return [Boolean] True if the XML is valid, otherwise False if soft=True
# @raise [ValidationError] if soft == false and validation fails
#
def validate_structure(soft = true)
def validate_structure
unless valid_saml?(document, soft)
return append_error(soft, "Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd")
return append_error("Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd")
end

true
Expand All @@ -240,12 +239,12 @@ def validate_structure(soft = true)
# @raise [ValidationError] if soft == false and validation fails
#
def validate_response_state(soft = true)
return append_error(soft, "Blank response") if response.empty?
return append_error("Blank response") if response.empty?

return append_error(soft, "No settings on response") if settings.nil?
return append_error("No settings on response") if settings.nil?

if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil?
return append_error(soft, "No fingerprint or certificate on settings")
return append_error("No fingerprint or certificate on settings")
end

true
Expand Down Expand Up @@ -274,39 +273,37 @@ def xpath_first_from_signed_assertion(subelt=nil)

# Validates the Conditions. (If the response was initialized with the :skip_conditions option, this validation is skipped,
# If the response was initialized with the :allowed_clock_drift option, the timing validations are relaxed by the allowed_clock_drift value)
# @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the response is invalid or not)
# @return [Boolean] True if satisfies the conditions, otherwise False if soft=True
# @raise [ValidationError] if soft == false and validation fails
#
def validate_conditions(soft = true)
def validate_conditions
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
error_msg = "Current time is earlier than NotBefore condition #{(now + (options[:allowed_clock_drift] || 0))} < #{not_before})"
return append_error(soft, error_msg)
return append_error(error_msg)
end

if not_on_or_after && now >= not_on_or_after
error_msg = "Current time is on or after NotOnOrAfter condition (#{now} >= #{not_on_or_after})"
return append_error(soft, error_msg)
return append_error(error_msg)
end

true
end

# Validates the Issuer (Of the SAML Response or of the SAML Assertion)
# @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the response is invalid or not)
# @return [Boolean] True if the Issuer matchs the IdP entityId, otherwise False if soft=True
# @raise [ValidationError] if soft == false and validation fails
#
def validate_issuer(soft = true)
def validate_issuer
return true if settings.idp_entity_id.nil?

unless URI.parse(issuer) == URI.parse(settings.idp_entity_id)
return append_error(soft, "Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>")
return append_error("Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>")
end
true
end
Expand Down
13 changes: 8 additions & 5 deletions lib/onelogin/ruby-saml/settings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ def initialize(overrides = {})
config = DEFAULTS.merge(overrides)
config.each do |k,v|
acc = "#{k.to_s}=".to_sym
if self.respond_to? acc
if respond_to? acc
value = v.is_a?(Hash) ? v.dup : v
self.send(acc, value)
send(acc, value)
end
end
@attribute_consuming_service = AttributeService.new
Expand Down Expand Up @@ -43,13 +43,15 @@ def initialize(overrides = {})
attr_accessor :protocol_binding
attr_accessor :attributes_index
attr_accessor :force_authn
attr_accessor :security
attr_accessor :certificate
attr_accessor :private_key
attr_accessor :authn_context
attr_accessor :authn_context_comparison
attr_accessor :authn_context_decl_ref
attr_reader :attribute_consuming_service
# Work-flow
attr_accessor :security
attr_accessor :soft
# Compability
attr_accessor :assertion_consumer_logout_service_url
attr_accessor :assertion_consumer_logout_service_binding
Expand Down Expand Up @@ -102,10 +104,10 @@ def single_logout_service_binding=(url)
# @return [String] The fingerprint
#
def get_fingerprint
self.idp_cert_fingerprint || begin
idp_cert_fingerprint || begin
idp_cert = get_idp_cert
if idp_cert
fingerprint_alg = XMLSecurity::BaseDocument.new.algorithm(self.idp_cert_fingerprint_algorithm).new
fingerprint_alg = XMLSecurity::BaseDocument.new.algorithm(idp_cert_fingerprint_algorithm).new
fingerprint_alg.hexdigest(idp_cert.to_der).upcase.scan(/../).join(":")
end
end
Expand Down Expand Up @@ -146,6 +148,7 @@ def get_sp_key
:idp_cert_fingerprint_algorithm => XMLSecurity::Document::SHA1,
:compress_request => true,
:compress_response => true,
:soft => true,
:security => {
:authn_requests_signed => false,
:logout_requests_signed => false,
Expand Down
Loading