ruby-saml 0.8.9 → 0.8.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ruby-saml might be problematic. Click here for more details.

Files changed (42) hide show
  1. data/Gemfile +11 -1
  2. data/Rakefile +0 -14
  3. data/lib/onelogin/ruby-saml/authrequest.rb +84 -18
  4. data/lib/onelogin/ruby-saml/logoutrequest.rb +93 -18
  5. data/lib/onelogin/ruby-saml/logoutresponse.rb +1 -24
  6. data/lib/onelogin/ruby-saml/response.rb +206 -11
  7. data/lib/onelogin/ruby-saml/setting_error.rb +6 -0
  8. data/lib/onelogin/ruby-saml/settings.rb +73 -12
  9. data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +158 -0
  10. data/lib/onelogin/ruby-saml/utils.rb +169 -0
  11. data/lib/onelogin/ruby-saml/version.rb +1 -1
  12. data/lib/ruby-saml.rb +2 -1
  13. data/lib/xml_security.rb +332 -78
  14. data/test/certificates/ruby-saml-2.crt +15 -0
  15. data/test/certificates/ruby-saml.crt +14 -0
  16. data/test/certificates/ruby-saml.key +15 -0
  17. data/test/logoutrequest_test.rb +177 -44
  18. data/test/logoutresponse_test.rb +23 -28
  19. data/test/request_test.rb +100 -37
  20. data/test/response_test.rb +337 -129
  21. data/test/responses/adfs_response_xmlns.xml +45 -0
  22. data/test/responses/encrypted_new_attack.xml.base64 +1 -0
  23. data/test/responses/invalids/multiple_signed.xml.base64 +1 -0
  24. data/test/responses/invalids/no_signature.xml.base64 +1 -0
  25. data/test/responses/invalids/response_with_concealed_signed_assertion.xml +51 -0
  26. data/test/responses/invalids/response_with_doubled_signed_assertion.xml +49 -0
  27. data/test/responses/invalids/signature_wrapping_attack.xml.base64 +1 -0
  28. data/test/responses/response_with_concealed_signed_assertion.xml +51 -0
  29. data/test/responses/response_with_doubled_signed_assertion.xml +49 -0
  30. data/test/responses/response_with_signed_assertion_3.xml +30 -0
  31. data/test/responses/response_with_signed_message_and_assertion.xml +34 -0
  32. data/test/responses/response_with_undefined_recipient.xml.base64 +1 -0
  33. data/test/responses/response_wrapped.xml.base64 +150 -0
  34. data/test/responses/valid_response.xml.base64 +1 -0
  35. data/test/responses/valid_response_without_x509certificate.xml.base64 +1 -0
  36. data/test/settings_test.rb +5 -5
  37. data/test/slo_logoutresponse_test.rb +226 -0
  38. data/test/test_helper.rb +117 -12
  39. data/test/utils_test.rb +10 -10
  40. data/test/xml_security_test.rb +354 -68
  41. metadata +64 -18
  42. checksums.yaml +0 -7
@@ -0,0 +1,6 @@
1
+ module OneLogin
2
+ module RubySaml
3
+ class SettingError < StandardError
4
+ end
5
+ end
6
+ end
@@ -1,27 +1,52 @@
1
+ require "xml_security"
2
+ require "onelogin/ruby-saml/utils"
3
+
1
4
  module OneLogin
2
5
  module RubySaml
3
6
  class Settings
4
- def initialize(overrides = {})
5
- config = DEFAULTS.merge(overrides)
6
- config.each do |k,v|
7
- acc = "#{k.to_s}=".to_sym
8
- self.send(acc, v) if self.respond_to? acc
9
- end
7
+ def initialize(overrides = {}, keep_security_attributes = false)
8
+ if keep_security_attributes
9
+ security_attributes = overrides.delete(:security) || {}
10
+ config = DEFAULTS.merge(overrides)
11
+ config[:security] = DEFAULTS[:security].merge(security_attributes)
12
+ else
13
+ config = DEFAULTS.merge(overrides)
14
+ end
15
+
16
+ config.each do |k,v|
17
+ acc = "#{k.to_s}=".to_sym
18
+ if respond_to? acc
19
+ value = v.is_a?(Hash) ? v.dup : v
20
+ send(acc, value)
21
+ end
22
+ end
10
23
  end
11
- attr_accessor :assertion_consumer_service_url, :sp_entity_id, :sp_name_qualifier
12
- attr_accessor :idp_sso_target_url, :idp_cert_fingerprint, :idp_cert, :name_identifier_format
13
- attr_accessor :authn_context
24
+
25
+ #idp data
26
+ attr_accessor :idp_sso_target_url
27
+ attr_accessor :idp_cert_fingerprint
28
+ attr_accessor :idp_cert
14
29
  attr_accessor :idp_slo_target_url
30
+ #sp data
31
+ attr_accessor :sp_entity_id
32
+ attr_accessor :assertion_consumer_service_url
33
+ attr_accessor :authn_context
34
+ attr_accessor :sp_name_qualifier
35
+ attr_accessor :name_identifier_format
15
36
  attr_accessor :name_identifier_value
16
37
  attr_accessor :name_identifier_value_requested
17
38
  attr_accessor :sessionindex
18
39
  attr_accessor :assertion_consumer_logout_service_url
19
40
  attr_accessor :compress_request
41
+ attr_accessor :compress_response
20
42
  attr_accessor :double_quote_xml_attribute_values
21
43
  attr_accessor :force_authn
22
44
  attr_accessor :passive
23
45
  attr_accessor :protocol_binding
24
-
46
+ attr_accessor :certificate
47
+ attr_accessor :private_key
48
+ # Work-flow
49
+ attr_accessor :security
25
50
  # Compability
26
51
  attr_accessor :issuer
27
52
  attr_accessor :assertion_consumer_logout_service_url
@@ -92,14 +117,50 @@ module OneLogin
92
117
  @single_logout_service_binding = url
93
118
  end
94
119
 
120
+ # @return [OpenSSL::X509::Certificate|nil] Build the SP certificate from the settings (previously format it)
121
+ #
122
+ def get_sp_cert
123
+ return nil if certificate.nil? || certificate.empty?
124
+
125
+ formatted_cert = OneLogin::RubySaml::Utils.format_cert(certificate)
126
+ OpenSSL::X509::Certificate.new(formatted_cert)
127
+ end
128
+
129
+ # @return [OpenSSL::X509::Certificate|nil] Build the New SP certificate from the settings (previously format it)
130
+ #
131
+ def get_sp_cert_new
132
+ return nil if certificate_new.nil? || certificate_new.empty?
133
+
134
+ formatted_cert = OneLogin::RubySaml::Utils.format_cert(certificate_new)
135
+ OpenSSL::X509::Certificate.new(formatted_cert)
136
+ end
137
+
138
+ # @return [OpenSSL::PKey::RSA] Build the SP private from the settings (previously format it)
139
+ #
140
+ def get_sp_key
141
+ return nil if private_key.nil? || private_key.empty?
142
+
143
+ formatted_private_key = OneLogin::RubySaml::Utils.format_private_key(private_key)
144
+ OpenSSL::PKey::RSA.new(formatted_private_key)
145
+ end
146
+
95
147
  private
96
148
 
97
149
  DEFAULTS = {
98
150
  :compress_request => true,
151
+ :compress_response => true,
99
152
  :double_quote_xml_attribute_values => false,
100
153
  :assertion_consumer_service_binding => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST".freeze,
101
- :single_logout_service_binding => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect".freeze
102
- }
154
+ :single_logout_service_binding => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect".freeze,
155
+ :security => {
156
+ :authn_requests_signed => false,
157
+ :logout_requests_signed => false,
158
+ :logout_responses_signed => false,
159
+ :embed_sign => false,
160
+ :digest_method => XMLSecurity::Document::SHA1,
161
+ :signature_method => XMLSecurity::Document::RSA_SHA1
162
+ }.freeze
163
+ }.freeze
103
164
  end
104
165
  end
105
166
  end
@@ -0,0 +1,158 @@
1
+ require "base64"
2
+ require "zlib"
3
+ require "cgi"
4
+ require "onelogin/ruby-saml/utils"
5
+ require "onelogin/ruby-saml/setting_error"
6
+
7
+ module OneLogin
8
+ module RubySaml
9
+
10
+ # SAML2 Logout Response (SLO SP initiated)
11
+ #
12
+ class SloLogoutresponse
13
+
14
+ # Logout Response ID
15
+ attr_reader :uuid
16
+
17
+ # Initializes the Logout Response. A SloLogoutresponse Object.
18
+ # Asigns an ID, a random uuid.
19
+ #
20
+ def initialize
21
+ @uuid = OneLogin::RubySaml::Utils.uuid
22
+ end
23
+
24
+ # Creates the Logout Response string.
25
+ # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
26
+ # @param request_id [String] The ID of the LogoutRequest sent by this SP to the IdP. That ID will be placed as the InResponseTo in the logout response
27
+ # @param logout_message [String] The Message to be placed as StatusMessage in the logout response
28
+ # @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
29
+ # @return [String] Logout Request string that includes the SAMLRequest
30
+ #
31
+ def create(settings, request_id = nil, logout_message = nil, params = {})
32
+ params = create_params(settings, request_id, logout_message, params)
33
+ params_prefix = (settings.idp_slo_target_url =~ /\?/) ? '&' : '?'
34
+ saml_response = CGI.escape(params.delete("SAMLResponse"))
35
+ response_params = "#{params_prefix}SAMLResponse=#{saml_response}"
36
+ params.each_pair do |key, value|
37
+ response_params << "&#{key.to_s}=#{CGI.escape(value.to_s)}"
38
+ end
39
+ raise SettingError.new "Invalid settings, idp_slo_target_url is not set!" if settings.idp_slo_target_url.nil? or settings.idp_slo_target_url.empty?
40
+ @logout_url = settings.idp_slo_target_url + response_params
41
+ end
42
+
43
+ # Creates the Get parameters for the logout response.
44
+ # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
45
+ # @param request_id [String] The ID of the LogoutRequest sent by this SP to the IdP. That ID will be placed as the InResponseTo in the logout response
46
+ # @param logout_message [String] The Message to be placed as StatusMessage in the logout response
47
+ # @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
48
+ # @return [Hash] Parameters
49
+ #
50
+ def create_params(settings, request_id = nil, logout_message = nil, params = {})
51
+ # The method expects :RelayState but sometimes we get 'RelayState' instead.
52
+ # Based on the HashWithIndifferentAccess value in Rails we could experience
53
+ # conflicts so this line will solve them.
54
+ relay_state = params[:RelayState] || params['RelayState']
55
+
56
+ if relay_state.nil?
57
+ params.delete(:RelayState)
58
+ params.delete('RelayState')
59
+ end
60
+
61
+ response_doc = create_logout_response_xml_doc(settings, request_id, logout_message)
62
+ response_doc.context[:attribute_quote] = :quote if settings.double_quote_xml_attribute_values
63
+
64
+ response = ""
65
+ response_doc.write(response)
66
+
67
+ Logging.debug "Created SLO Logout Response: #{response}"
68
+
69
+ response = Zlib::Deflate.deflate(response, 9)[2..-5] if settings.compress_response
70
+ if Base64.respond_to?('strict_encode64')
71
+ base64_response = Base64.strict_encode64(response)
72
+ else
73
+ base64_response = Base64.encode64(response).gsub(/\n/, "")
74
+ end
75
+ response_params = {"SAMLResponse" => base64_response}
76
+
77
+ if settings.security[:logout_responses_signed] && !settings.security[:embed_sign] && settings.private_key
78
+ params['SigAlg'] = settings.security[:signature_method]
79
+ url_string = OneLogin::RubySaml::Utils.build_query(
80
+ :type => 'SAMLResponse',
81
+ :data => base64_response,
82
+ :relay_state => relay_state,
83
+ :sig_alg => params['SigAlg']
84
+ )
85
+ sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method])
86
+ signature = settings.get_sp_key.sign(sign_algorithm.new, url_string)
87
+ if Base64.respond_to?('strict_encode64')
88
+ params['Signature'] = Base64.strict_encode64(signature)
89
+ else
90
+ params['Signature'] = Base64.encode64(signature).gsub(/\n/, "")
91
+ end
92
+ end
93
+
94
+ params.each_pair do |key, value|
95
+ response_params[key] = value.to_s
96
+ end
97
+
98
+ response_params
99
+ end
100
+
101
+ # Creates the SAMLResponse String.
102
+ # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
103
+ # @param request_id [String] The ID of the LogoutRequest sent by this SP to the IdP. That ID will be placed as the InResponseTo in the logout response
104
+ # @param logout_message [String] The Message to be placed as StatusMessage in the logout response
105
+ # @return [String] The SAMLResponse String.
106
+ #
107
+ def create_logout_response_xml_doc(settings, request_id = nil, logout_message = nil)
108
+ document = create_xml_document(settings, request_id, logout_message)
109
+ sign_document(document, settings)
110
+ end
111
+
112
+ def create_xml_document(settings, request_id = nil, logout_message = nil)
113
+ time = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
114
+
115
+ response_doc = XMLSecurity::Document.new
116
+ response_doc.uuid = uuid
117
+
118
+ root = response_doc.add_element 'samlp:LogoutResponse', { 'xmlns:samlp' => 'urn:oasis:names:tc:SAML:2.0:protocol', "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion" }
119
+ root.attributes['ID'] = uuid
120
+ root.attributes['IssueInstant'] = time
121
+ root.attributes['Version'] = '2.0'
122
+ root.attributes['InResponseTo'] = request_id unless request_id.nil?
123
+ root.attributes['Destination'] = settings.idp_slo_target_url unless settings.idp_slo_target_url.nil? or settings.idp_slo_target_url.empty?
124
+
125
+ if settings.sp_entity_id != nil
126
+ issuer = root.add_element "saml:Issuer"
127
+ issuer.text = settings.sp_entity_id
128
+ end
129
+
130
+ # add success message
131
+ status = root.add_element 'samlp:Status'
132
+
133
+ # success status code
134
+ status_code = status.add_element 'samlp:StatusCode'
135
+ status_code.attributes['Value'] = 'urn:oasis:names:tc:SAML:2.0:status:Success'
136
+
137
+ # success status message
138
+ logout_message ||= 'Successfully Signed Out'
139
+ status_message = status.add_element 'samlp:StatusMessage'
140
+ status_message.text = logout_message
141
+
142
+ response_doc
143
+ end
144
+
145
+ def sign_document(document, settings)
146
+ # embed signature
147
+ if settings.security[:logout_responses_signed] && settings.private_key && settings.certificate && settings.security[:embed_sign]
148
+ private_key = settings.get_sp_key
149
+ cert = settings.get_sp_cert
150
+ document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
151
+ end
152
+
153
+ document
154
+ end
155
+
156
+ end
157
+ end
158
+ end
@@ -1,15 +1,184 @@
1
+ if RUBY_VERSION < '1.9'
2
+ require 'uuid'
3
+ else
4
+ require 'securerandom'
5
+ end
6
+
7
+ require "base64"
8
+ require "zlib"
9
+
1
10
  module OneLogin
2
11
  module RubySaml
3
12
 
4
13
  # SAML2 Auxiliary class
5
14
  #
6
15
  class Utils
16
+ @@uuid_generator = UUID.new if RUBY_VERSION < '1.9'
17
+
18
+ BASE64_FORMAT = %r(\A([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?\Z)
19
+
7
20
  # Given a REXML::Element instance, return the concatenation of all child text nodes. Assumes
8
21
  # that there all children other than text nodes can be ignored (e.g. comments). If nil is
9
22
  # passed, nil will be returned.
10
23
  def self.element_text(element)
11
24
  element.texts.map(&:value).join if element
12
25
  end
26
+
27
+ # Return a properly formatted x509 certificate
28
+ #
29
+ # @param cert [String] The original certificate
30
+ # @return [String] The formatted certificate
31
+ #
32
+ def self.format_cert(cert)
33
+ # don't try to format an encoded certificate or if is empty or nil
34
+ if cert.respond_to?(:ascii_only?)
35
+ return cert if cert.nil? || cert.empty? || !cert.ascii_only?
36
+ else
37
+ return cert if cert.nil? || cert.empty? || cert.match(/\x0d/)
38
+ end
39
+
40
+ if cert.scan(/BEGIN CERTIFICATE/).length > 1
41
+ formatted_cert = []
42
+ cert.scan(/-{5}BEGIN CERTIFICATE-{5}[\n\r]?.*?-{5}END CERTIFICATE-{5}[\n\r]?/m) {|c|
43
+ formatted_cert << format_cert(c)
44
+ }
45
+ formatted_cert.join("\n")
46
+ else
47
+ cert = cert.gsub(/\-{5}\s?(BEGIN|END) CERTIFICATE\s?\-{5}/, "")
48
+ cert = cert.gsub(/\r/, "")
49
+ cert = cert.gsub(/\n/, "")
50
+ cert = cert.gsub(/\s/, "")
51
+ cert = cert.scan(/.{1,64}/)
52
+ cert = cert.join("\n")
53
+ "-----BEGIN CERTIFICATE-----\n#{cert}\n-----END CERTIFICATE-----"
54
+ end
55
+ end
56
+
57
+ # Return a properly formatted private key
58
+ #
59
+ # @param key [String] The original private key
60
+ # @return [String] The formatted private key
61
+ #
62
+ def self.format_private_key(key)
63
+ # don't try to format an encoded private key or if is empty
64
+ return key if key.nil? || key.empty? || key.match(/\x0d/)
65
+
66
+ # is this an rsa key?
67
+ rsa_key = key.match("RSA PRIVATE KEY")
68
+ key = key.gsub(/\-{5}\s?(BEGIN|END)( RSA)? PRIVATE KEY\s?\-{5}/, "")
69
+ key = key.gsub(/\n/, "")
70
+ key = key.gsub(/\r/, "")
71
+ key = key.gsub(/\s/, "")
72
+ key = key.scan(/.{1,64}/)
73
+ key = key.join("\n")
74
+ key_label = rsa_key ? "RSA PRIVATE KEY" : "PRIVATE KEY"
75
+ "-----BEGIN #{key_label}-----\n#{key}\n-----END #{key_label}-----"
76
+ end
77
+
78
+ # Build the Query String signature that will be used in the HTTP-Redirect binding
79
+ # to generate the Signature
80
+ # @param params [Hash] Parameters to build the Query String
81
+ # @option params [String] :type 'SAMLRequest' or 'SAMLResponse'
82
+ # @option params [String] :data Base64 encoded SAMLRequest or SAMLResponse
83
+ # @option params [String] :relay_state The RelayState parameter
84
+ # @option params [String] :sig_alg The SigAlg parameter
85
+ # @return [String] The Query String
86
+ #
87
+ def self.build_query(params)
88
+ type, data, relay_state, sig_alg = [:type, :data, :relay_state, :sig_alg].map { |k| params[k]}
89
+ url_string = "#{type}=#{CGI.escape(data)}"
90
+ url_string << "&RelayState=#{CGI.escape(relay_state)}" if relay_state
91
+ url_string << "&SigAlg=#{CGI.escape(sig_alg)}"
92
+ end
93
+
94
+ def self.uuid
95
+ RUBY_VERSION < '1.9' ? "_#{@@uuid_generator.generate}" : "_#{SecureRandom.uuid}"
96
+ end
97
+
98
+ # Build the status error message
99
+ # @param status_code [String] StatusCode value
100
+ # @param status_message [Strig] StatusMessage value
101
+ # @return [String] The status error message
102
+ def self.status_error_msg(error_msg, raw_status_code = nil, status_message = nil)
103
+ unless raw_status_code.nil?
104
+ if raw_status_code.include? "|"
105
+ status_codes = raw_status_code.split(' | ')
106
+ values = status_codes.collect do |status_code|
107
+ status_code.split(':').last
108
+ end
109
+ printable_code = values.join(" => ")
110
+ else
111
+ printable_code = raw_status_code.split(':').last
112
+ end
113
+ error_msg << ', was ' + printable_code
114
+ end
115
+
116
+ unless status_message.nil?
117
+ error_msg << ' -> ' + status_message
118
+ end
119
+
120
+ error_msg
121
+ end
122
+
123
+ # Base64 decode and try also to inflate a SAML Message
124
+ # @param saml [String] The deflated and encoded SAML Message
125
+ # @return [String] The plain SAML Message
126
+ #
127
+ def self.decode_raw_saml(saml)
128
+ return saml unless base64_encoded?(saml)
129
+
130
+ decoded = decode(saml)
131
+ begin
132
+ inflate(decoded)
133
+ rescue
134
+ decoded
135
+ end
136
+ end
137
+
138
+ # Base 64 decode method
139
+ # @param string [String] The string message
140
+ # @return [String] The decoded string
141
+ #
142
+ def self.decode(string)
143
+ Base64.decode64(string)
144
+ end
145
+
146
+ # Base 64 encode method
147
+ # @param string [String] The string
148
+ # @return [String] The encoded string
149
+ #
150
+ def self.encode(string)
151
+ if Base64.respond_to?('strict_encode64')
152
+ Base64.strict_encode64(string)
153
+ else
154
+ Base64.encode64(string).gsub(/\n/, "")
155
+ end
156
+ end
157
+
158
+ # Check if a string is base64 encoded
159
+ # @param string [String] string to check the encoding of
160
+ # @return [true, false] whether or not the string is base64 encoded
161
+ #
162
+ def self.base64_encoded?(string)
163
+ !!string.gsub(/[\r\n]|\\r|\\n|\s/, "").match(BASE64_FORMAT)
164
+ end
165
+
166
+ # Inflate method
167
+ # @param deflated [String] The string
168
+ # @return [String] The inflated string
169
+ #
170
+ def self.inflate(deflated)
171
+ Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(deflated)
172
+ end
173
+
174
+ # Deflate method
175
+ # @param inflated [String] The string
176
+ # @return [String] The deflated string
177
+ #
178
+ def self.deflate(inflated)
179
+ Zlib::Deflate.deflate(inflated, 9)[2..-5]
180
+ end
181
+
13
182
  end
14
183
  end
15
184
  end