Skip to content
22 changes: 12 additions & 10 deletions UPGRADING.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Ruby SAML Migration Guide

## Updating from 1.x to 2.0.0
## Upgrading from 1.x to 2.0.0

**IMPORTANT: Please read this section carefully as it contains breaking changes!**

Expand Down Expand Up @@ -34,7 +34,7 @@ Note that the project folder structure has also been updated accordingly. Notabl
`lib/onelogin/schemas` is now `lib/ruby_saml/schemas`.

For backward compatibility, the alias `OneLogin = Object` has been set, so `OneLogin::RubySaml::` will still work
as before. This alias will be removed in RubySaml version `2.1.0`.
as before. This alias will be removed in RubySaml version `3.0.0`.

### Deprecation and removal of "XMLSecurity" namespace

Expand Down Expand Up @@ -101,7 +101,7 @@ end

RubySaml now always uses double quotes for attribute values when generating XML.
The `settings.double_quote_xml_attribute_values` parameter now always behaves as `true`,
and will be removed in RubySaml 2.1.0.
and will be removed in RubySaml 3.0.0.

The reasons for this change are:
- RubySaml will use Nokogiri instead of REXML to generate XML. Nokogiri does not support
Expand Down Expand Up @@ -154,7 +154,7 @@ a different `sp_uuid_prefix` is passed-in on subsequent calls.
### Deprecation of compression settings

The `settings.compress_request` and `settings.compress_response` parameters have been deprecated
and are no longer functional. They will be removed in RubySaml 2.1.0. Please remove `compress_request`
and are no longer functional. They will be removed in RubySaml 3.0.0. Please remove `compress_request`
and `compress_response` everywhere within your project code.

The SAML SP request/response message compression behavior is now controlled automatically by the
Expand All @@ -166,13 +166,15 @@ compression may be achieved by enabling `Content-Encoding: gzip` on your webserv
### Deprecation of IdP certificate fingerprint settings

The `settings.idp_cert_fingerprint` and `settings.idp_cert_fingerprint_algorithm` are deprecated
and will be removed in RubySaml 2.1.0. Please use `settings.idp_cert` or `settings.idp_cert_multi` instead.
The reasons for this deprecation are that (1) fingerprint cannot be used with HTTP-Redirect binding,
and (2) fingerprint is theoretically susceptible to collision attacks.
and will be removed in RubySaml 3.0.0. Please use `settings.idp_cert` or `settings.idp_cert_multi` instead.

The reasons for this deprecation are:
- Fingerprint cannot be used with HTTP-Redirect binding
- Fingerprint is theoretically susceptible to collision attacks.

### Other settings deprecations

The following parameters in `RubySaml::Settings` are deprecated and will be removed in RubySaml 2.1.0:
The following parameters in `RubySaml::Settings` are deprecated and will be removed in RubySaml 3.0.0:

- `#issuer` is deprecated and replaced 1:1 by `#sp_entity_id`
- `#idp_sso_target_url` is deprecated and replaced 1:1 by `#idp_sso_service_url`
Expand Down Expand Up @@ -212,7 +214,7 @@ and `#format_private_key` methods. Specifically:
stripped out.
- Case 7: If no valid certificates are found, the entire original string will be returned.

## Updating from 1.17.x to 1.18.0
## Upgrading from 1.17.x to 1.18.0

Version `1.18.0` changes the way the toolkit validates SAML signatures. There is a new order
how validation happens in the toolkit and also the toolkit by default will check malformed doc
Expand All @@ -222,7 +224,7 @@ The SignedDocument class defined at xml_security.rb experienced several changes.
We don't expect compatibilty issues if you use the main methods offered by ruby-saml, but if
you use a fork or customized usage, is possible that you need to adapt your code.

## Updating from 1.12.x to 1.13.0
## Upgrading from 1.12.x to 1.13.0

Version `1.13.0` adds `settings.idp_sso_service_binding` and `settings.idp_slo_service_binding`, and
deprecates `settings.security[:embed_sign]`. If specified, new binding parameters will be used in place of `:embed_sign`
Expand Down
2 changes: 1 addition & 1 deletion lib/ruby_saml.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@
require 'ruby_saml/utils'
require 'ruby_saml/version'

# @deprecated This alias adds compatibility with v1.x and will be removed in v2.1.0
# @deprecated This alias adds compatibility with v1.x and will be removed in v3.0.0
OneLogin = Object
6 changes: 3 additions & 3 deletions lib/ruby_saml/idp_metadata_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ def parse_to_array(idp_metadata, options = {})
end

def parse_to_idp_metadata_array(idp_metadata, options = {})
@document = Nokogiri::XML(idp_metadata)
@document = Nokogiri::XML(idp_metadata) # TODO: RubySaml::XML.safe_load_nokogiri
@options = options

idpsso_descriptors = self.class.get_idps(@document, options[:entity_id])
Expand Down Expand Up @@ -348,14 +348,14 @@ def certificates
unless signing_nodes.empty?
certs['signing'] = []
signing_nodes.each do |cert_node|
certs['signing'] << cert_node.content
certs['signing'] << cert_node.text
end
end

unless encryption_nodes.empty?
certs['encryption'] = []
encryption_nodes.each do |cert_node|
certs['encryption'] << cert_node.content
certs['encryption'] << cert_node.text
end
end
certs
Expand Down
46 changes: 17 additions & 29 deletions lib/ruby_saml/logoutresponse.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def initialize(response, settings = nil, options = {})

@options = options
@response = RubySaml::XML::Decoder.decode_message(response, @settings&.message_max_bytesize)
@document = RubySaml::XML::SignedDocument.new(@response)
@document = RubySaml::XML.safe_load_nokogiri(@response)
super()
end

Expand All @@ -60,47 +60,35 @@ def success?
# @return [String|nil] Gets the InResponseTo attribute from the Logout Response if exists.
#
def in_response_to
@in_response_to ||= begin
node = REXML::XPath.first(
document,
"/p:LogoutResponse",
{ "p" => RubySaml::XML::NS_PROTOCOL }
)
node.nil? ? nil : node.attributes['InResponseTo']
end
@in_response_to ||= document.at_xpath(
"/p:LogoutResponse",
{ "p" => RubySaml::XML::NS_PROTOCOL }
)&.[]('InResponseTo')
end

# @return [String] Gets the Issuer from the Logout Response.
#
def issuer
@issuer ||= begin
node = REXML::XPath.first(
document,
"/p:LogoutResponse/a:Issuer",
{ "p" => RubySaml::XML::NS_PROTOCOL, "a" => RubySaml::XML::NS_ASSERTION }
)
Utils.element_text(node)
end
@issuer ||= document.at_xpath(
"/p:LogoutResponse/a:Issuer",
{ "p" => RubySaml::XML::NS_PROTOCOL, "a" => RubySaml::XML::NS_ASSERTION }
)&.text
end

# @return [String] Gets the StatusCode from a Logout Response.
#
def status_code
@status_code ||= begin
node = REXML::XPath.first(document, "/p:LogoutResponse/p:Status/p:StatusCode", { "p" => RubySaml::XML::NS_PROTOCOL })
node.nil? ? nil : node.attributes["Value"]
end
@status_code ||= document.at_xpath(
"/p:LogoutResponse/p:Status/p:StatusCode",
{ "p" => RubySaml::XML::NS_PROTOCOL }
)&.[]('Value')
end

def status_message
@status_message ||= begin
node = REXML::XPath.first(
document,
"/p:LogoutResponse/p:Status/p:StatusMessage",
{ "p" => RubySaml::XML::NS_PROTOCOL }
)
Utils.element_text(node)
end
@status_message ||= document.at_xpath(
"/p:LogoutResponse/p:Status/p:StatusMessage",
{ "p" => RubySaml::XML::NS_PROTOCOL }
)&.text
end

# Aux function to validate the Logout Response
Expand Down
66 changes: 66 additions & 0 deletions lib/ruby_saml/memoizable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# frozen_string_literal: true

module RubySaml
# Mixin for memoizing methods
module Memoizable
# Creates a memoized method
#
# @param method_name [Symbol] the name of the method to memoize
# @param original_method [Symbol, nil] the original method to memoize (defaults to method_name)
def self.included(base)
base.extend(ClassMethods)
end

private

# Memoizes the result of a block using the given name as the cache key
#
# @param cache_key [Symbol, String] the name to use as the cache key
# @yield the block whose result will be cached
# @return [Object] the cached result or the result of the block
def memoize(cache_key)
cache_key = "@#{cache_key.to_s.delete_prefix('@')}"
return instance_variable_get(cache_key) if instance_variable_defined?(cache_key)

instance_variable_set(cache_key, yield)
end

# Class methods for memoization
module ClassMethods
# Defines multiple memoized methods
#
# @param method_names [Array<Symbol>] the names of the methods to memoize
# @raise [ArgumentError] if any method has an arity greater than 0
def memoize_method(*method_names)
method_names.each do |method_name|
method_obj = instance_method(method_name)

# Check method arity
if method_obj.arity > 0 # rubocop:disable Style/IfUnlessModifier
raise ArgumentError.new("Cannot memoize method '#{method_name}' with arity > 0")
end

# Store the original method
original_method_name = "#{method_name}_without_memoization"
alias_method original_method_name, method_name
private original_method_name

# Define the memoized version
define_method(method_name) do |&block|
cache_key = "@memoized_#{method_name}"
memoize(cache_key) do
send(original_method_name, &block)
end
end

# Preserve method visibility
if private_method_defined?(original_method_name)
private method_name
elsif protected_method_defined?(original_method_name)
protected method_name
end
end
end
end
end
end
Loading