ruby-saml 0.8.12

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 (70) hide show
  1. checksums.yaml +7 -0
  2. data/.document +5 -0
  3. data/.gitignore +12 -0
  4. data/.travis.yml +11 -0
  5. data/Gemfile +37 -0
  6. data/LICENSE +19 -0
  7. data/README.md +160 -0
  8. data/Rakefile +27 -0
  9. data/changelog.md +24 -0
  10. data/lib/onelogin/ruby-saml/attributes.rb +147 -0
  11. data/lib/onelogin/ruby-saml/authrequest.rb +168 -0
  12. data/lib/onelogin/ruby-saml/logging.rb +26 -0
  13. data/lib/onelogin/ruby-saml/logoutrequest.rb +161 -0
  14. data/lib/onelogin/ruby-saml/logoutresponse.rb +153 -0
  15. data/lib/onelogin/ruby-saml/metadata.rb +66 -0
  16. data/lib/onelogin/ruby-saml/response.rb +426 -0
  17. data/lib/onelogin/ruby-saml/setting_error.rb +6 -0
  18. data/lib/onelogin/ruby-saml/settings.rb +166 -0
  19. data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +158 -0
  20. data/lib/onelogin/ruby-saml/utils.rb +119 -0
  21. data/lib/onelogin/ruby-saml/validation_error.rb +7 -0
  22. data/lib/onelogin/ruby-saml/version.rb +5 -0
  23. data/lib/ruby-saml.rb +12 -0
  24. data/lib/schemas/saml20assertion_schema.xsd +283 -0
  25. data/lib/schemas/saml20protocol_schema.xsd +302 -0
  26. data/lib/schemas/xenc_schema.xsd +146 -0
  27. data/lib/schemas/xmldsig_schema.xsd +318 -0
  28. data/lib/xml_security.rb +292 -0
  29. data/ruby-saml.gemspec +28 -0
  30. data/test/certificates/certificate1 +12 -0
  31. data/test/certificates/r1_certificate2_base64 +1 -0
  32. data/test/certificates/ruby-saml.crt +14 -0
  33. data/test/certificates/ruby-saml.key +15 -0
  34. data/test/logoutrequest_test.rb +244 -0
  35. data/test/logoutresponse_test.rb +112 -0
  36. data/test/request_test.rb +229 -0
  37. data/test/response_test.rb +475 -0
  38. data/test/responses/adfs_response_sha1.xml +46 -0
  39. data/test/responses/adfs_response_sha256.xml +46 -0
  40. data/test/responses/adfs_response_sha384.xml +46 -0
  41. data/test/responses/adfs_response_sha512.xml +46 -0
  42. data/test/responses/encrypted_new_attack.xml.base64 +1 -0
  43. data/test/responses/logoutresponse_fixtures.rb +67 -0
  44. data/test/responses/no_signature_ns.xml +48 -0
  45. data/test/responses/open_saml_response.xml +56 -0
  46. data/test/responses/r1_response6.xml.base64 +1 -0
  47. data/test/responses/response1.xml.base64 +1 -0
  48. data/test/responses/response2.xml.base64 +79 -0
  49. data/test/responses/response3.xml.base64 +66 -0
  50. data/test/responses/response4.xml.base64 +93 -0
  51. data/test/responses/response5.xml.base64 +102 -0
  52. data/test/responses/response_eval.xml +7 -0
  53. data/test/responses/response_node_text_attack.xml.base64 +1 -0
  54. data/test/responses/response_with_ampersands.xml +139 -0
  55. data/test/responses/response_with_ampersands.xml.base64 +93 -0
  56. data/test/responses/response_with_concealed_signed_assertion.xml +51 -0
  57. data/test/responses/response_with_doubled_signed_assertion.xml +49 -0
  58. data/test/responses/response_with_multiple_attribute_statements.xml +72 -0
  59. data/test/responses/response_with_multiple_attribute_values.xml +67 -0
  60. data/test/responses/response_wrapped.xml.base64 +150 -0
  61. data/test/responses/simple_saml_php.xml +71 -0
  62. data/test/responses/starfield_response.xml.base64 +1 -0
  63. data/test/responses/valid_response.xml.base64 +1 -0
  64. data/test/responses/wrapped_response_2.xml.base64 +150 -0
  65. data/test/settings_test.rb +47 -0
  66. data/test/slo_logoutresponse_test.rb +226 -0
  67. data/test/test_helper.rb +155 -0
  68. data/test/utils_test.rb +41 -0
  69. data/test/xml_security_test.rb +158 -0
  70. metadata +178 -0
@@ -0,0 +1,6 @@
1
+ module OneLogin
2
+ module RubySaml
3
+ class SettingError < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,166 @@
1
+ require "xml_security"
2
+ require "onelogin/ruby-saml/utils"
3
+
4
+ module OneLogin
5
+ module RubySaml
6
+ class Settings
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
23
+ end
24
+
25
+ #idp data
26
+ attr_accessor :idp_sso_target_url
27
+ attr_accessor :idp_cert_fingerprint
28
+ attr_accessor :idp_cert
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
36
+ attr_accessor :name_identifier_value
37
+ attr_accessor :name_identifier_value_requested
38
+ attr_accessor :sessionindex
39
+ attr_accessor :assertion_consumer_logout_service_url
40
+ attr_accessor :compress_request
41
+ attr_accessor :compress_response
42
+ attr_accessor :double_quote_xml_attribute_values
43
+ attr_accessor :force_authn
44
+ attr_accessor :passive
45
+ attr_accessor :protocol_binding
46
+ attr_accessor :certificate
47
+ attr_accessor :private_key
48
+ # Work-flow
49
+ attr_accessor :security
50
+ # Compability
51
+ attr_accessor :issuer
52
+ attr_accessor :assertion_consumer_logout_service_url
53
+ attr_accessor :assertion_consumer_logout_service_binding
54
+
55
+ # @return [String] SP Entity ID
56
+ #
57
+ def sp_entity_id
58
+ val = nil
59
+ if @sp_entity_id.nil?
60
+ if @issuer
61
+ val = @issuer
62
+ end
63
+ else
64
+ val = @sp_entity_id
65
+ end
66
+ val
67
+ end
68
+
69
+ # Setter for SP Entity ID.
70
+ # @param val [String].
71
+ #
72
+ def sp_entity_id=(val)
73
+ @sp_entity_id = val
74
+ end
75
+
76
+ # @return [String] Single Logout Service URL.
77
+ #
78
+ def single_logout_service_url
79
+ val = nil
80
+ if @single_logout_service_url.nil?
81
+ if @assertion_consumer_logout_service_url
82
+ val = @assertion_consumer_logout_service_url
83
+ end
84
+ else
85
+ val = @single_logout_service_url
86
+ end
87
+ val
88
+ end
89
+
90
+ # Setter for the Single Logout Service URL.
91
+ # @param url [String].
92
+ #
93
+ def single_logout_service_url=(url)
94
+ @single_logout_service_url = url
95
+ end
96
+
97
+ # @return [String] Single Logout Service Binding.
98
+ #
99
+ def single_logout_service_binding
100
+ val = nil
101
+ if @single_logout_service_binding.nil?
102
+ if @assertion_consumer_logout_service_binding
103
+ val = @assertion_consumer_logout_service_binding
104
+ end
105
+ else
106
+ val = @single_logout_service_binding
107
+ end
108
+ val
109
+ end
110
+
111
+ # Setter for Single Logout Service Binding.
112
+ #
113
+ # (Currently we only support "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect")
114
+ # @param url [String]
115
+ #
116
+ def single_logout_service_binding=(url)
117
+ @single_logout_service_binding = url
118
+ end
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
+
147
+ private
148
+
149
+ DEFAULTS = {
150
+ :compress_request => true,
151
+ :compress_response => true,
152
+ :double_quote_xml_attribute_values => false,
153
+ :assertion_consumer_service_binding => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST".freeze,
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
164
+ end
165
+ end
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
@@ -0,0 +1,119 @@
1
+ if RUBY_VERSION < '1.9'
2
+ require 'uuid'
3
+ else
4
+ require 'securerandom'
5
+ end
6
+
7
+ module OneLogin
8
+ module RubySaml
9
+
10
+ # SAML2 Auxiliary class
11
+ #
12
+ class Utils
13
+ @@uuid_generator = UUID.new if RUBY_VERSION < '1.9'
14
+
15
+ # Given a REXML::Element instance, return the concatenation of all child text nodes. Assumes
16
+ # that there all children other than text nodes can be ignored (e.g. comments). If nil is
17
+ # passed, nil will be returned.
18
+ def self.element_text(element)
19
+ element.texts.map(&:value).join if element
20
+ end
21
+
22
+ # Return a properly formatted x509 certificate
23
+ #
24
+ # @param cert [String] The original certificate
25
+ # @return [String] The formatted certificate
26
+ #
27
+ def self.format_cert(cert)
28
+ # don't try to format an encoded certificate or if is empty or nil
29
+ if cert.respond_to?(:ascii_only?)
30
+ return cert if cert.nil? || cert.empty? || !cert.ascii_only?
31
+ else
32
+ return cert if cert.nil? || cert.empty? || cert.match(/\x0d/)
33
+ end
34
+
35
+ if cert.scan(/BEGIN CERTIFICATE/).length > 1
36
+ formatted_cert = []
37
+ cert.scan(/-{5}BEGIN CERTIFICATE-{5}[\n\r]?.*?-{5}END CERTIFICATE-{5}[\n\r]?/m) {|c|
38
+ formatted_cert << format_cert(c)
39
+ }
40
+ formatted_cert.join("\n")
41
+ else
42
+ cert = cert.gsub(/\-{5}\s?(BEGIN|END) CERTIFICATE\s?\-{5}/, "")
43
+ cert = cert.gsub(/\r/, "")
44
+ cert = cert.gsub(/\n/, "")
45
+ cert = cert.gsub(/\s/, "")
46
+ cert = cert.scan(/.{1,64}/)
47
+ cert = cert.join("\n")
48
+ "-----BEGIN CERTIFICATE-----\n#{cert}\n-----END CERTIFICATE-----"
49
+ end
50
+ end
51
+
52
+ # Return a properly formatted private key
53
+ #
54
+ # @param key [String] The original private key
55
+ # @return [String] The formatted private key
56
+ #
57
+ def self.format_private_key(key)
58
+ # don't try to format an encoded private key or if is empty
59
+ return key if key.nil? || key.empty? || key.match(/\x0d/)
60
+
61
+ # is this an rsa key?
62
+ rsa_key = key.match("RSA PRIVATE KEY")
63
+ key = key.gsub(/\-{5}\s?(BEGIN|END)( RSA)? PRIVATE KEY\s?\-{5}/, "")
64
+ key = key.gsub(/\n/, "")
65
+ key = key.gsub(/\r/, "")
66
+ key = key.gsub(/\s/, "")
67
+ key = key.scan(/.{1,64}/)
68
+ key = key.join("\n")
69
+ key_label = rsa_key ? "RSA PRIVATE KEY" : "PRIVATE KEY"
70
+ "-----BEGIN #{key_label}-----\n#{key}\n-----END #{key_label}-----"
71
+ end
72
+
73
+ # Build the Query String signature that will be used in the HTTP-Redirect binding
74
+ # to generate the Signature
75
+ # @param params [Hash] Parameters to build the Query String
76
+ # @option params [String] :type 'SAMLRequest' or 'SAMLResponse'
77
+ # @option params [String] :data Base64 encoded SAMLRequest or SAMLResponse
78
+ # @option params [String] :relay_state The RelayState parameter
79
+ # @option params [String] :sig_alg The SigAlg parameter
80
+ # @return [String] The Query String
81
+ #
82
+ def self.build_query(params)
83
+ type, data, relay_state, sig_alg = [:type, :data, :relay_state, :sig_alg].map { |k| params[k]}
84
+ url_string = "#{type}=#{CGI.escape(data)}"
85
+ url_string << "&RelayState=#{CGI.escape(relay_state)}" if relay_state
86
+ url_string << "&SigAlg=#{CGI.escape(sig_alg)}"
87
+ end
88
+
89
+ def self.uuid
90
+ RUBY_VERSION < '1.9' ? "_#{@@uuid_generator.generate}" : "_#{SecureRandom.uuid}"
91
+ end
92
+
93
+ # Build the status error message
94
+ # @param status_code [String] StatusCode value
95
+ # @param status_message [Strig] StatusMessage value
96
+ # @return [String] The status error message
97
+ def self.status_error_msg(error_msg, raw_status_code = nil, status_message = nil)
98
+ unless raw_status_code.nil?
99
+ if raw_status_code.include? "|"
100
+ status_codes = raw_status_code.split(' | ')
101
+ values = status_codes.collect do |status_code|
102
+ status_code.split(':').last
103
+ end
104
+ printable_code = values.join(" => ")
105
+ else
106
+ printable_code = raw_status_code.split(':').last
107
+ end
108
+ error_msg << ', was ' + printable_code
109
+ end
110
+
111
+ unless status_message.nil?
112
+ error_msg << ' -> ' + status_message
113
+ end
114
+
115
+ error_msg
116
+ end
117
+ end
118
+ end
119
+ end