ruby-saml 0.8.9 → 0.8.14

Sign up to get free protection for your applications and to get access to all the features.

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