ruby-saml 0.8.12

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 (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