kl-ruby-saml 0.0.1

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.
Files changed (137) hide show
  1. checksums.yaml +7 -0
  2. data/.document +5 -0
  3. data/.gitignore +14 -0
  4. data/.travis.yml +17 -0
  5. data/Gemfile +9 -0
  6. data/LICENSE +19 -0
  7. data/README.md +575 -0
  8. data/Rakefile +41 -0
  9. data/changelog.md +75 -0
  10. data/gemfiles/nokogiri-1.5.gemfile +5 -0
  11. data/lib/onelogin/ruby-saml.rb +17 -0
  12. data/lib/onelogin/ruby-saml/attribute_service.rb +57 -0
  13. data/lib/onelogin/ruby-saml/attributes.rb +128 -0
  14. data/lib/onelogin/ruby-saml/authrequest.rb +156 -0
  15. data/lib/onelogin/ruby-saml/http_error.rb +7 -0
  16. data/lib/onelogin/ruby-saml/idp_metadata_parser.rb +161 -0
  17. data/lib/onelogin/ruby-saml/logging.rb +30 -0
  18. data/lib/onelogin/ruby-saml/logoutrequest.rb +131 -0
  19. data/lib/onelogin/ruby-saml/logoutresponse.rb +241 -0
  20. data/lib/onelogin/ruby-saml/metadata.rb +123 -0
  21. data/lib/onelogin/ruby-saml/response.rb +722 -0
  22. data/lib/onelogin/ruby-saml/saml_message.rb +158 -0
  23. data/lib/onelogin/ruby-saml/settings.rb +165 -0
  24. data/lib/onelogin/ruby-saml/slo_logoutrequest.rb +258 -0
  25. data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +136 -0
  26. data/lib/onelogin/ruby-saml/utils.rb +172 -0
  27. data/lib/onelogin/ruby-saml/validation_error.rb +7 -0
  28. data/lib/onelogin/ruby-saml/version.rb +5 -0
  29. data/lib/ruby-saml.rb +1 -0
  30. data/lib/schemas/saml-schema-assertion-2.0.xsd +283 -0
  31. data/lib/schemas/saml-schema-authn-context-2.0.xsd +23 -0
  32. data/lib/schemas/saml-schema-authn-context-types-2.0.xsd +821 -0
  33. data/lib/schemas/saml-schema-metadata-2.0.xsd +337 -0
  34. data/lib/schemas/saml-schema-protocol-2.0.xsd +302 -0
  35. data/lib/schemas/sstc-metadata-attr.xsd +35 -0
  36. data/lib/schemas/sstc-saml-attribute-ext.xsd +25 -0
  37. data/lib/schemas/sstc-saml-metadata-algsupport-v1.0.xsd +41 -0
  38. data/lib/schemas/sstc-saml-metadata-ui-v1.0.xsd +89 -0
  39. data/lib/schemas/xenc-schema.xsd +136 -0
  40. data/lib/schemas/xml.xsd +287 -0
  41. data/lib/schemas/xmldsig-core-schema.xsd +309 -0
  42. data/lib/xml_security.rb +358 -0
  43. data/ruby-saml.gemspec +57 -0
  44. data/test/certificates/certificate1 +12 -0
  45. data/test/certificates/certificate_without_head_foot +1 -0
  46. data/test/certificates/formatted_certificate +14 -0
  47. data/test/certificates/formatted_private_key +12 -0
  48. data/test/certificates/formatted_rsa_private_key +12 -0
  49. data/test/certificates/invalid_certificate1 +1 -0
  50. data/test/certificates/invalid_certificate2 +1 -0
  51. data/test/certificates/invalid_certificate3 +12 -0
  52. data/test/certificates/invalid_private_key1 +1 -0
  53. data/test/certificates/invalid_private_key2 +1 -0
  54. data/test/certificates/invalid_private_key3 +10 -0
  55. data/test/certificates/invalid_rsa_private_key1 +1 -0
  56. data/test/certificates/invalid_rsa_private_key2 +1 -0
  57. data/test/certificates/invalid_rsa_private_key3 +10 -0
  58. data/test/certificates/ruby-saml.crt +14 -0
  59. data/test/certificates/ruby-saml.key +15 -0
  60. data/test/idp_metadata_parser_test.rb +95 -0
  61. data/test/logging_test.rb +62 -0
  62. data/test/logout_requests/invalid_slo_request.xml +6 -0
  63. data/test/logout_requests/slo_request.xml +4 -0
  64. data/test/logout_requests/slo_request.xml.base64 +1 -0
  65. data/test/logout_requests/slo_request_deflated.xml.base64 +1 -0
  66. data/test/logout_requests/slo_request_with_session_index.xml +5 -0
  67. data/test/logout_responses/logoutresponse_fixtures.rb +67 -0
  68. data/test/logoutrequest_test.rb +211 -0
  69. data/test/logoutresponse_test.rb +258 -0
  70. data/test/metadata_test.rb +203 -0
  71. data/test/request_test.rb +282 -0
  72. data/test/response_test.rb +1094 -0
  73. data/test/responses/adfs_response_sha1.xml +46 -0
  74. data/test/responses/adfs_response_sha256.xml +46 -0
  75. data/test/responses/adfs_response_sha384.xml +46 -0
  76. data/test/responses/adfs_response_sha512.xml +46 -0
  77. data/test/responses/adfs_response_xmlns.xml +45 -0
  78. data/test/responses/attackxee.xml +13 -0
  79. data/test/responses/idp_descriptor.xml +3 -0
  80. data/test/responses/invalids/invalid_audience.xml.base64 +1 -0
  81. data/test/responses/invalids/invalid_issuer_assertion.xml.base64 +1 -0
  82. data/test/responses/invalids/invalid_issuer_message.xml.base64 +1 -0
  83. data/test/responses/invalids/invalid_signature_position.xml.base64 +1 -0
  84. data/test/responses/invalids/invalid_subjectconfirmation_inresponse.xml.base64 +1 -0
  85. data/test/responses/invalids/invalid_subjectconfirmation_nb.xml.base64 +1 -0
  86. data/test/responses/invalids/invalid_subjectconfirmation_noa.xml.base64 +1 -0
  87. data/test/responses/invalids/invalid_subjectconfirmation_recipient.xml.base64 +1 -0
  88. data/test/responses/invalids/multiple_assertions.xml.base64 +2 -0
  89. data/test/responses/invalids/multiple_signed.xml.base64 +1 -0
  90. data/test/responses/invalids/no_id.xml.base64 +1 -0
  91. data/test/responses/invalids/no_saml2.xml.base64 +1 -0
  92. data/test/responses/invalids/no_signature.xml.base64 +1 -0
  93. data/test/responses/invalids/no_status.xml.base64 +1 -0
  94. data/test/responses/invalids/no_status_code.xml.base64 +1 -0
  95. data/test/responses/invalids/no_subjectconfirmation_data.xml.base64 +1 -0
  96. data/test/responses/invalids/no_subjectconfirmation_method.xml.base64 +1 -0
  97. data/test/responses/invalids/response_encrypted_attrs.xml.base64 +1 -0
  98. data/test/responses/invalids/response_invalid_signed_element.xml.base64 +1 -0
  99. data/test/responses/invalids/status_code_responder.xml.base64 +1 -0
  100. data/test/responses/invalids/status_code_responer_and_msg.xml.base64 +1 -0
  101. data/test/responses/no_signature_ns.xml +48 -0
  102. data/test/responses/open_saml_response.xml +56 -0
  103. data/test/responses/response_assertion_wrapped.xml.base64 +93 -0
  104. data/test/responses/response_encrypted_nameid.xml.base64 +1 -0
  105. data/test/responses/response_eval.xml +7 -0
  106. data/test/responses/response_no_cert_and_encrypted_attrs.xml +29 -0
  107. data/test/responses/response_unsigned_xml_base64 +1 -0
  108. data/test/responses/response_with_ampersands.xml +139 -0
  109. data/test/responses/response_with_ampersands.xml.base64 +93 -0
  110. data/test/responses/response_with_multiple_attribute_values.xml +67 -0
  111. data/test/responses/response_with_saml2_namespace.xml.base64 +102 -0
  112. data/test/responses/response_with_signed_assertion.xml.base64 +66 -0
  113. data/test/responses/response_with_signed_assertion_2.xml.base64 +1 -0
  114. data/test/responses/response_with_undefined_recipient.xml.base64 +1 -0
  115. data/test/responses/response_without_attributes.xml.base64 +79 -0
  116. data/test/responses/response_wrapped.xml.base64 +150 -0
  117. data/test/responses/signed_message_encrypted_signed_assertion.xml.base64 +1 -0
  118. data/test/responses/signed_message_encrypted_unsigned_assertion.xml.base64 +1 -0
  119. data/test/responses/simple_saml_php.xml +71 -0
  120. data/test/responses/starfield_response.xml.base64 +1 -0
  121. data/test/responses/test_sign.xml +43 -0
  122. data/test/responses/unsigned_message_aes128_encrypted_signed_assertion.xml.base64 +1 -0
  123. data/test/responses/unsigned_message_aes192_encrypted_signed_assertion.xml.base64 +1 -0
  124. data/test/responses/unsigned_message_aes256_encrypted_signed_assertion.xml.base64 +1 -0
  125. data/test/responses/unsigned_message_des192_encrypted_signed_assertion.xml.base64 +1 -0
  126. data/test/responses/unsigned_message_encrypted_assertion_without_saml_namespace.xml.base64 +1 -0
  127. data/test/responses/unsigned_message_encrypted_signed_assertion.xml.base64 +1 -0
  128. data/test/responses/unsigned_message_encrypted_unsigned_assertion.xml.base64 +1 -0
  129. data/test/responses/valid_response.xml.base64 +1 -0
  130. data/test/saml_message_test.rb +56 -0
  131. data/test/settings_test.rb +218 -0
  132. data/test/slo_logoutrequest_test.rb +275 -0
  133. data/test/slo_logoutresponse_test.rb +185 -0
  134. data/test/test_helper.rb +252 -0
  135. data/test/utils_test.rb +145 -0
  136. data/test/xml_security_test.rb +329 -0
  137. metadata +415 -0
@@ -0,0 +1,123 @@
1
+ require "uri"
2
+ require "uuid"
3
+
4
+ require "onelogin/ruby-saml/logging"
5
+
6
+ # Only supports SAML 2.0
7
+ module OneLogin
8
+ module RubySaml
9
+
10
+ # SAML2 Metadata. XML Metadata Builder
11
+ #
12
+ class Metadata
13
+
14
+ # Return SP metadata based on the settings.
15
+ # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
16
+ # @param pretty_print [Boolean] Pretty print or not the response
17
+ # (No pretty print if you gonna validate the signature)
18
+ # @return [String] XML Metadata of the Service Provider
19
+ #
20
+ def generate(settings, pretty_print=false)
21
+ meta_doc = XMLSecurity::Document.new
22
+ namespaces = {
23
+ "xmlns:md" => "urn:oasis:names:tc:SAML:2.0:metadata"
24
+ }
25
+ if settings.attribute_consuming_service.configured?
26
+ namespaces["xmlns:saml"] = "urn:oasis:names:tc:SAML:2.0:assertion"
27
+ end
28
+ root = meta_doc.add_element "md:EntityDescriptor", namespaces
29
+ sp_sso = root.add_element "md:SPSSODescriptor", {
30
+ "protocolSupportEnumeration" => "urn:oasis:names:tc:SAML:2.0:protocol",
31
+ "AuthnRequestsSigned" => settings.security[:authn_requests_signed],
32
+ # However we would like assertions signed if idp_cert_fingerprint or idp_cert is set
33
+ "WantAssertionsSigned" => !!(settings.idp_cert_fingerprint || settings.idp_cert)
34
+ }
35
+
36
+ # Add KeyDescriptor if messages will be signed / encrypted
37
+ cert = settings.get_sp_cert
38
+ if cert
39
+ cert_text = Base64.encode64(cert.to_der).gsub("\n", '')
40
+ kd = sp_sso.add_element "md:KeyDescriptor", { "use" => "signing" }
41
+ ki = kd.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"}
42
+ xd = ki.add_element "ds:X509Data"
43
+ xc = xd.add_element "ds:X509Certificate"
44
+ xc.text = cert_text
45
+
46
+ kd2 = sp_sso.add_element "md:KeyDescriptor", { "use" => "encryption" }
47
+ ki2 = kd2.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"}
48
+ xd2 = ki2.add_element "ds:X509Data"
49
+ xc2 = xd2.add_element "ds:X509Certificate"
50
+ xc2.text = cert_text
51
+ end
52
+
53
+ root.attributes["ID"] = "_" + UUID.new.generate
54
+ if settings.issuer
55
+ root.attributes["entityID"] = settings.issuer
56
+ end
57
+ if settings.single_logout_service_url
58
+ sp_sso.add_element "md:SingleLogoutService", {
59
+ "Binding" => settings.single_logout_service_binding,
60
+ "Location" => settings.single_logout_service_url,
61
+ "ResponseLocation" => settings.single_logout_service_url
62
+ }
63
+ end
64
+ if settings.name_identifier_format
65
+ nameid = sp_sso.add_element "md:NameIDFormat"
66
+ nameid.text = settings.name_identifier_format
67
+ end
68
+ if settings.assertion_consumer_service_url
69
+ sp_sso.add_element "md:AssertionConsumerService", {
70
+ "Binding" => settings.assertion_consumer_service_binding,
71
+ "Location" => settings.assertion_consumer_service_url,
72
+ "isDefault" => true,
73
+ "index" => 0
74
+ }
75
+ end
76
+
77
+ if settings.attribute_consuming_service.configured?
78
+ sp_acs = sp_sso.add_element "md:AttributeConsumingService", {
79
+ "isDefault" => "true",
80
+ "index" => settings.attribute_consuming_service.index
81
+ }
82
+ srv_name = sp_acs.add_element "md:ServiceName", {
83
+ "xml:lang" => "en"
84
+ }
85
+ srv_name.text = settings.attribute_consuming_service.name
86
+ settings.attribute_consuming_service.attributes.each do |attribute|
87
+ sp_req_attr = sp_acs.add_element "md:RequestedAttribute", {
88
+ "NameFormat" => attribute[:name_format],
89
+ "Name" => attribute[:name],
90
+ "FriendlyName" => attribute[:friendly_name]
91
+ }
92
+ unless attribute[:attribute_value].nil?
93
+ sp_attr_val = sp_req_attr.add_element "saml:AttributeValue"
94
+ sp_attr_val.text = attribute[:attribute_value]
95
+ end
96
+ end
97
+ end
98
+
99
+ # With OpenSSO, it might be required to also include
100
+ # <md:RoleDescriptor xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:query="urn:oasis:names:tc:SAML:metadata:ext:query" xsi:type="query:AttributeQueryDescriptorType" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"/>
101
+ # <md:XACMLAuthzDecisionQueryDescriptor WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"/>
102
+
103
+ meta_doc << REXML::XMLDecl.new("1.0", "UTF-8")
104
+
105
+ # embed signature
106
+ if settings.security[:metadata_signed] && settings.private_key && settings.certificate
107
+ private_key = settings.get_sp_key
108
+ meta_doc.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
109
+ end
110
+
111
+ ret = ""
112
+ # pretty print the XML so IdP administrators can easily see what the SP supports
113
+ if pretty_print
114
+ meta_doc.write(ret, 1)
115
+ else
116
+ ret = meta_doc.to_s
117
+ end
118
+
119
+ return ret
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,722 @@
1
+ require "xml_security"
2
+ require "onelogin/ruby-saml/attributes"
3
+
4
+ require "time"
5
+ require "nokogiri"
6
+
7
+ # Only supports SAML 2.0
8
+ module OneLogin
9
+ module RubySaml
10
+
11
+ # SAML2 Authentication Response. SAML Response
12
+ #
13
+ class Response < SamlMessage
14
+ ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
15
+ PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
16
+ DSIG = "http://www.w3.org/2000/09/xmldsig#"
17
+ XENC = "http://www.w3.org/2001/04/xmlenc#"
18
+
19
+ # TODO: Settings should probably be initialized too... WDYT?
20
+
21
+ # OneLogin::RubySaml::Settings Toolkit settings
22
+ attr_accessor :settings
23
+
24
+ # Array with the causes [Array of strings]
25
+ attr_accessor :errors
26
+
27
+ attr_reader :document
28
+ attr_reader :decrypted_document
29
+ attr_reader :response
30
+ attr_reader :options
31
+
32
+ attr_accessor :soft
33
+
34
+ # Constructs the SAML Response. A Response Object that is an extension of the SamlMessage class.
35
+ # @param response [String] A UUEncoded SAML response from the IdP.
36
+ # @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object
37
+ # Or some options for the response validation process like skip the conditions validation
38
+ # with the :skip_conditions, or allow a clock_drift when checking dates with :allowed_clock_drift
39
+ # or :matches_request_id that will validate that the response matches the ID of the request.
40
+ def initialize(response, options = {})
41
+ @errors = []
42
+
43
+ raise ArgumentError.new("Response cannot be nil") if response.nil?
44
+ @options = options
45
+
46
+ @soft = true
47
+ if !options.empty? && !options[:settings].nil?
48
+ @settings = options[:settings]
49
+ if !options[:settings].soft.nil?
50
+ @soft = options[:settings].soft
51
+ end
52
+ end
53
+
54
+ @response = decode_raw_saml(response)
55
+ @document = XMLSecurity::SignedDocument.new(@response, @errors)
56
+
57
+ if assertion_encrypted?
58
+ @decrypted_document = generate_decrypted_document
59
+ end
60
+ end
61
+
62
+ # Append the cause to the errors array, and based on the value of soft, return false or raise
63
+ # an exception
64
+ def append_error(error_msg)
65
+ @errors << error_msg
66
+ return soft ? false : validation_error(error_msg)
67
+ end
68
+
69
+ # Reset the errors array
70
+ def reset_errors!
71
+ @errors = []
72
+ end
73
+
74
+ # Validates the SAML Response with the default values (soft = true)
75
+ # @return [Boolean] TRUE if the SAML Response is valid
76
+ #
77
+ def is_valid?
78
+ validate
79
+ end
80
+
81
+ # @return [String] the NameID provided by the SAML response from the IdP.
82
+ #
83
+ def name_id
84
+ @name_id ||= begin
85
+ encrypted_node = xpath_first_from_signed_assertion('/a:Subject/a:EncryptedID')
86
+ if encrypted_node
87
+ node = decrypt_nameid(encrypted_node)
88
+ else
89
+ node = xpath_first_from_signed_assertion('/a:Subject/a:NameID')
90
+ end
91
+ node.nil? ? nil : node.text
92
+ end
93
+ end
94
+
95
+ alias_method :nameid, :name_id
96
+
97
+ # Gets the SessionIndex from the AuthnStatement.
98
+ # Could be used to be stored in the local session in order
99
+ # to be used in a future Logout Request that the SP could
100
+ # send to the IdP, to set what specific session must be deleted
101
+ # @return [String] SessionIndex Value
102
+ #
103
+ def sessionindex
104
+ @sessionindex ||= begin
105
+ node = xpath_first_from_signed_assertion('/a:AuthnStatement')
106
+ node.nil? ? nil : node.attributes['SessionIndex']
107
+ end
108
+ end
109
+
110
+ # Gets the Attributes from the AttributeStatement element.
111
+ #
112
+ # All attributes can be iterated over +attributes.each+ or returned as array by +attributes.all+
113
+ # For backwards compatibility ruby-saml returns by default only the first value for a given attribute with
114
+ # attributes['name']
115
+ # To get all of the attributes, use:
116
+ # attributes.multi('name')
117
+ # Or turn off the compatibility:
118
+ # OneLogin::RubySaml::Attributes.single_value_compatibility = false
119
+ # Now this will return an array:
120
+ # attributes['name']
121
+ #
122
+ # @return [Attributes] OneLogin::RubySaml::Attributes enumerable collection.
123
+ #
124
+ def attributes
125
+ @attr_statements ||= begin
126
+ attributes = Attributes.new
127
+
128
+ stmt_element = xpath_first_from_signed_assertion('/a:AttributeStatement')
129
+ return attributes if stmt_element.nil?
130
+
131
+ stmt_element.elements.each do |attr_element|
132
+ name = attr_element.attributes["Name"]
133
+ values = attr_element.elements.collect{|e|
134
+ # SAMLCore requires that nil AttributeValues MUST contain xsi:nil XML attribute set to "true" or "1"
135
+ # otherwise the value is to be regarded as empty.
136
+ ["true", "1"].include?(e.attributes['xsi:nil']) ? nil : e.text.to_s
137
+ }
138
+
139
+ attributes.add(name, values)
140
+ end
141
+
142
+ attributes
143
+ end
144
+ end
145
+
146
+ # Gets the SessionNotOnOrAfter from the AuthnStatement.
147
+ # Could be used to set the local session expiration (expire at latest)
148
+ # @return [String] The SessionNotOnOrAfter value
149
+ #
150
+ def session_expires_at
151
+ @expires_at ||= begin
152
+ node = xpath_first_from_signed_assertion('/a:AuthnStatement')
153
+ node.nil? ? nil : parse_time(node, "SessionNotOnOrAfter")
154
+ end
155
+ end
156
+
157
+ # Checks if the Status has the "Success" code
158
+ # @return [Boolean] True if the StatusCode is Sucess
159
+ #
160
+ def success?
161
+ status_code == "urn:oasis:names:tc:SAML:2.0:status:Success"
162
+ end
163
+
164
+ # @return [String] StatusCode value from a SAML Response.
165
+ #
166
+ def status_code
167
+ @status_code ||= begin
168
+ node = REXML::XPath.first(
169
+ document,
170
+ "/p:Response/p:Status/p:StatusCode",
171
+ { "p" => PROTOCOL, "a" => ASSERTION }
172
+ )
173
+ node.attributes["Value"] if node && node.attributes
174
+ end
175
+ end
176
+
177
+ # @return [String] the StatusMessage value from a SAML Response.
178
+ #
179
+ def status_message
180
+ @status_message ||= begin
181
+ node = REXML::XPath.first(
182
+ document,
183
+ "/p:Response/p:Status/p:StatusMessage",
184
+ { "p" => PROTOCOL, "a" => ASSERTION }
185
+ )
186
+ node.text if node
187
+ end
188
+ end
189
+
190
+ # Gets the Condition Element of the SAML Response if exists.
191
+ # (returns the first node that matches the supplied xpath)
192
+ # @return [REXML::Element] Conditions Element if exists
193
+ #
194
+ def conditions
195
+ @conditions ||= xpath_first_from_signed_assertion('/a:Conditions')
196
+ end
197
+
198
+ # Gets the NotBefore Condition Element value.
199
+ # @return [Time] The NotBefore value in Time format
200
+ #
201
+ def not_before
202
+ @not_before ||= parse_time(conditions, "NotBefore")
203
+ end
204
+
205
+ # Gets the NotOnOrAfter Condition Element value.
206
+ # @return [Time] The NotOnOrAfter value in Time format
207
+ #
208
+ def not_on_or_after
209
+ @not_on_or_after ||= parse_time(conditions, "NotOnOrAfter")
210
+ end
211
+
212
+ # Gets the Issuers (from Response and Assertion).
213
+ # (returns the first node that matches the supplied xpath from the Response and from the Assertion)
214
+ # @return [Array] Array with the Issuers (REXML::Element)
215
+ #
216
+ def issuers
217
+ @issuers ||= begin
218
+ issuers = []
219
+ nodes = REXML::XPath.match(
220
+ document,
221
+ "/p:Response/a:Issuer",
222
+ { "p" => PROTOCOL, "a" => ASSERTION }
223
+ )
224
+ nodes += xpath_from_signed_assertion("/a:Issuer")
225
+ nodes.each do |node|
226
+ issuers << node.text if node.text
227
+ end
228
+ issuers.uniq
229
+ end
230
+ end
231
+
232
+ # @return [String|nil] The InResponseTo attribute from the SAML Response.
233
+ #
234
+ def in_response_to
235
+ @in_response_to ||= begin
236
+ node = REXML::XPath.first(
237
+ document,
238
+ "/p:Response",
239
+ { "p" => PROTOCOL }
240
+ )
241
+ node.nil? ? nil : node.attributes['InResponseTo']
242
+ end
243
+ end
244
+
245
+ # @return [Array] The Audience elements from the Contitions of the SAML Response.
246
+ #
247
+ def audiences
248
+ @audiences ||= begin
249
+ audiences = []
250
+ nodes = xpath_from_signed_assertion('/a:Conditions/a:AudienceRestriction/a:Audience')
251
+ nodes.each do |node|
252
+ if node && node.text
253
+ audiences << node.text
254
+ end
255
+ end
256
+ audiences
257
+ end
258
+ end
259
+
260
+ # returns the allowed clock drift on timing validation
261
+ # @return [Integer]
262
+ def allowed_clock_drift
263
+ return options[:allowed_clock_drift] || 0
264
+ end
265
+
266
+ private
267
+
268
+ # Validates the SAML Response (calls several validation methods)
269
+ # @return [Boolean] True if the SAML Response is valid, otherwise False if soft=True
270
+ # @raise [ValidationError] if soft == false and validation fails
271
+ #
272
+ def validate
273
+ reset_errors!
274
+
275
+ validate_response_state &&
276
+ validate_version &&
277
+ validate_id &&
278
+ validate_success_status &&
279
+ validate_num_assertion &&
280
+ validate_no_encrypted_attributes &&
281
+ validate_signed_elements &&
282
+ validate_structure &&
283
+ validate_in_response_to &&
284
+ validate_conditions &&
285
+ validate_audience &&
286
+ validate_issuer &&
287
+ validate_session_expiration &&
288
+ validate_subject_confirmation &&
289
+ validate_signature
290
+ end
291
+
292
+
293
+ # Validates the Status of the SAML Response
294
+ # @return [Boolean] True if the SAML Response contains a Success code, otherwise False if soft == false
295
+ # @raise [ValidationError] if soft == false and validation fails
296
+ #
297
+ def validate_success_status
298
+ return true if success?
299
+
300
+ error_msg = 'The status code of the Response was not Success'
301
+ status_error_msg = OneLogin::RubySaml::Utils.status_error_msg(error_msg, status_code, status_message)
302
+ append_error(status_error_msg)
303
+ end
304
+
305
+ # Validates the SAML Response against the specified schema.
306
+ # @return [Boolean] True if the XML is valid, otherwise False if soft=True
307
+ # @raise [ValidationError] if soft == false and validation fails
308
+ #
309
+ def validate_structure
310
+ unless valid_saml?(document, soft)
311
+ return append_error("Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd")
312
+ end
313
+
314
+ true
315
+ end
316
+
317
+ # Validates that the SAML Response provided in the initialization is not empty,
318
+ # also check that the setting and the IdP cert were also provided
319
+ # @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the response is invalid or not)
320
+ # @return [Boolean] True if the required info is found, otherwise False if soft=True
321
+ # @raise [ValidationError] if soft == false and validation fails
322
+ #
323
+ def validate_response_state(soft = true)
324
+ return append_error("Blank response") if response.nil? || response.empty?
325
+
326
+ return append_error("No settings on response") if settings.nil?
327
+
328
+ if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil?
329
+ return append_error("No fingerprint or certificate on settings")
330
+ end
331
+
332
+ true
333
+ end
334
+
335
+ # Validates that the SAML Response contains an ID
336
+ # If fails, the error is added to the errors array.
337
+ # @return [Boolean] True if the SAML Response contains an ID, otherwise returns False
338
+ #
339
+ def validate_id
340
+ unless id(document)
341
+ return append_error("Missing ID attribute on SAML Response")
342
+ end
343
+
344
+ true
345
+ end
346
+
347
+ # Validates the SAML version (2.0)
348
+ # If fails, the error is added to the errors array.
349
+ # @return [Boolean] True if the SAML Response is 2.0, otherwise returns False
350
+ #
351
+ def validate_version
352
+ unless version(document) == "2.0"
353
+ return append_error("Unsupported SAML version")
354
+ end
355
+
356
+ true
357
+ end
358
+
359
+ # Validates that the SAML Response only contains a single Assertion (encrypted or not).
360
+ # If fails, the error is added to the errors array.
361
+ # @return [Boolean] True if the SAML Response contains one unique Assertion, otherwise False
362
+ #
363
+ def validate_num_assertion
364
+ assertions = REXML::XPath.match(
365
+ document,
366
+ "//a:Assertion",
367
+ { "a" => ASSERTION }
368
+ )
369
+ encrypted_assertions = REXML::XPath.match(
370
+ document,
371
+ "//a:EncryptedAssertion",
372
+ { "a" => ASSERTION }
373
+ )
374
+
375
+ unless assertions.size + encrypted_assertions.size == 1
376
+ return append_error("SAML Response must contain 1 assertion")
377
+ end
378
+
379
+ true
380
+ end
381
+
382
+ # Validates that there are not EncryptedAttribute (not supported)
383
+ # If fails, the error is added to the errors array
384
+ # @return [Boolean] True if there are no EncryptedAttribute elements, otherwise False if soft=True
385
+ # @raise [ValidationError] if soft == false and validation fails
386
+ #
387
+ def validate_no_encrypted_attributes
388
+ nodes = xpath_from_signed_assertion("/a:AttributeStatement/a:EncryptedAttribute")
389
+ if nodes && nodes.length > 0
390
+ return append_error("There is an EncryptedAttribute in the Response and this SP not support them")
391
+ end
392
+
393
+ true
394
+ end
395
+
396
+
397
+ # Validates the Signed elements
398
+ # If fails, the error is added to the errors array
399
+ # @return [Boolean] True if there is 1 or 2 Elements signed in the SAML Response
400
+ # an are a Response or an Assertion Element, otherwise False if soft=True
401
+ #
402
+ def validate_signed_elements
403
+ signature_nodes = REXML::XPath.match(
404
+ decrypted_document.nil? ? document : decrypted_document,
405
+ "//ds:Signature",
406
+ {"ds"=>DSIG}
407
+ )
408
+ signed_elements = []
409
+ signature_nodes.each do |signature_node|
410
+ signed_element = signature_node.parent.name
411
+ if signed_element != 'Response' && signed_element != 'Assertion'
412
+ return append_error("Found an unexpected Signature Element. SAML Response rejected")
413
+ end
414
+ signed_elements << signed_element
415
+ end
416
+
417
+ unless signature_nodes.length < 3 && !signed_elements.empty?
418
+ return append_error("Found an unexpected number of Signature Element. SAML Response rejected")
419
+ end
420
+
421
+ true
422
+ end
423
+
424
+ # Validates if the provided request_id match the inResponseTo value.
425
+ # If fails, the error is added to the errors array
426
+ # @return [Boolean] True if there is no request_id or it match, otherwise False if soft=True
427
+ # @raise [ValidationError] if soft == false and validation fails
428
+ #
429
+ def validate_in_response_to
430
+ return true unless options.has_key? :matches_request_id
431
+ return true if options[:matches_request_id].nil? || options[:matches_request_id].empty?
432
+ return true unless options[:matches_request_id] != in_response_to
433
+
434
+ error_msg = "The InResponseTo of the Response: #{in_response_to}, does not match the ID of the AuthNRequest sent by the SP: #{options[:matches_request_id]}"
435
+ append_error(error_msg)
436
+ end
437
+
438
+ # Validates the Audience, (If the Audience match the Service Provider EntityID)
439
+ # If fails, the error is added to the errors array
440
+ # @return [Boolean] True if there is an Audience Element that match the Service Provider EntityID, otherwise False if soft=True
441
+ # @raise [ValidationError] if soft == false and validation fails
442
+ #
443
+ def validate_audience
444
+ return true if audiences.empty? || settings.issuer.nil? || settings.issuer.empty?
445
+
446
+ unless audiences.include? settings.issuer
447
+ error_msg = "#{settings.issuer} is not a valid audience for this Response"
448
+ return append_error(error_msg)
449
+ end
450
+
451
+ true
452
+ end
453
+
454
+ # Validates the Conditions. (If the response was initialized with the :skip_conditions option, this validation is skipped,
455
+ # If the response was initialized with the :allowed_clock_drift option, the timing validations are relaxed by the allowed_clock_drift value)
456
+ # @return [Boolean] True if satisfies the conditions, otherwise False if soft=True
457
+ # @raise [ValidationError] if soft == false and validation fails
458
+ #
459
+ def validate_conditions
460
+ return true if conditions.nil?
461
+ return true if options[:skip_conditions]
462
+
463
+ now = Time.now.utc
464
+
465
+ if not_before && (now + allowed_clock_drift) < not_before
466
+ error_msg = "Current time is earlier than NotBefore condition #{(now + allowed_clock_drift)} < #{not_before})"
467
+ return append_error(error_msg)
468
+ end
469
+
470
+ if not_on_or_after && now >= (not_on_or_after + allowed_clock_drift)
471
+ error_msg = "Current time is on or after NotOnOrAfter condition (#{now} >= #{not_on_or_after + allowed_clock_drift})"
472
+ return append_error(error_msg)
473
+ end
474
+
475
+ true
476
+ end
477
+
478
+ # Validates the Issuer (Of the SAML Response and the SAML Assertion)
479
+ # @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the response is invalid or not)
480
+ # @return [Boolean] True if the Issuer matchs the IdP entityId, otherwise False if soft=True
481
+ # @raise [ValidationError] if soft == false and validation fails
482
+ #
483
+ def validate_issuer
484
+ return true if settings.idp_entity_id.nil?
485
+
486
+ issuers.each do |issuer|
487
+ unless URI.parse(issuer) == URI.parse(settings.idp_entity_id)
488
+ error_msg = "Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>"
489
+ return append_error(error_msg)
490
+ end
491
+ end
492
+
493
+ true
494
+ end
495
+
496
+ # Validates that the Session haven't expired (If the response was initialized with the :allowed_clock_drift option,
497
+ # this time validation is relaxed by the allowed_clock_drift value)
498
+ # If fails, the error is added to the errors array
499
+ # @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the response is invalid or not)
500
+ # @return [Boolean] True if the SessionNotOnOrAfter of the AttributeStatement is valid, otherwise (when expired) False if soft=True
501
+ # @raise [ValidationError] if soft == false and validation fails
502
+ #
503
+ def validate_session_expiration(soft = true)
504
+ return true if session_expires_at.nil?
505
+
506
+ now = Time.now.utc
507
+ unless session_expires_at > (now + allowed_clock_drift)
508
+ error_msg = "The attributes have expired, based on the SessionNotOnOrAfter of the AttributeStatement of this Response"
509
+ return append_error(error_msg)
510
+ end
511
+
512
+ true
513
+ end
514
+
515
+ # Validates if exists valid SubjectConfirmation (If the response was initialized with the :allowed_clock_drift option,
516
+ # timimg validation are relaxed by the allowed_clock_drift value)
517
+ # If fails, the error is added to the errors array
518
+ # @return [Boolean] True if exists a valid SubjectConfirmation, otherwise False if soft=True
519
+ # @raise [ValidationError] if soft == false and validation fails
520
+ #
521
+ def validate_subject_confirmation
522
+ valid_subject_confirmation = false
523
+
524
+ subject_confirmation_nodes = xpath_from_signed_assertion('/a:Subject/a:SubjectConfirmation')
525
+
526
+ now = Time.now.utc
527
+ subject_confirmation_nodes.each do |subject_confirmation|
528
+ if subject_confirmation.attributes.include? "Method" and subject_confirmation.attributes['Method'] != 'urn:oasis:names:tc:SAML:2.0:cm:bearer'
529
+ next
530
+ end
531
+
532
+ confirmation_data_node = REXML::XPath.first(
533
+ subject_confirmation,
534
+ 'a:SubjectConfirmationData',
535
+ { "a" => ASSERTION }
536
+ )
537
+
538
+ next unless confirmation_data_node
539
+
540
+ attrs = confirmation_data_node.attributes
541
+ next if (attrs.include? "InResponseTo" and attrs['InResponseTo'] != in_response_to) ||
542
+ (attrs.include? "NotOnOrAfter" and (parse_time(confirmation_data_node, "NotOnOrAfter") + allowed_clock_drift) <= now) ||
543
+ (attrs.include? "NotBefore" and parse_time(confirmation_data_node, "NotBefore") > (now + allowed_clock_drift))
544
+
545
+ valid_subject_confirmation = true
546
+ break
547
+ end
548
+
549
+ if !valid_subject_confirmation
550
+ error_msg = "A valid SubjectConfirmation was not found on this Response"
551
+ return append_error(error_msg)
552
+ end
553
+
554
+ true
555
+ end
556
+
557
+ # Validates the Signature
558
+ # @return [Boolean] True if not contains a Signature or if the Signature is valid, otherwise False if soft=True
559
+ # @raise [ValidationError] if soft == false and validation fails
560
+ #
561
+ def validate_signature
562
+ fingerprint = settings.get_fingerprint
563
+
564
+ # If the response contains the signature, and the assertion was encrypted, validate the original SAML Response
565
+ # otherwise, review if the decrypted assertion contains a signature
566
+ response_signed = REXML::XPath.first(
567
+ document,
568
+ "/p:Response[@ID=$id]",
569
+ { "p" => PROTOCOL, "ds" => DSIG },
570
+ { 'id' => document.signed_element_id }
571
+ )
572
+ doc = (response_signed || decrypted_document.nil?) ? document : decrypted_document
573
+
574
+ unless fingerprint && doc.validate_document(fingerprint, :fingerprint_alg => settings.idp_cert_fingerprint_algorithm)
575
+ error_msg = "Invalid Signature on SAML Response"
576
+ return append_error(error_msg)
577
+ end
578
+
579
+ true
580
+ end
581
+
582
+ # Extracts the first appearance that matchs the subelt (pattern)
583
+ # Search on any Assertion that is signed, or has a Response parent signed
584
+ # @param subelt [String] The XPath pattern
585
+ # @return [REXML::Element | nil] If any matches, return the Element
586
+ #
587
+ def xpath_first_from_signed_assertion(subelt=nil)
588
+ doc = decrypted_document.nil? ? document : decrypted_document
589
+ node = REXML::XPath.first(
590
+ doc,
591
+ "/p:Response/a:Assertion[@ID=$id]#{subelt}",
592
+ { "p" => PROTOCOL, "a" => ASSERTION },
593
+ { 'id' => doc.signed_element_id }
594
+ )
595
+ node ||= REXML::XPath.first(
596
+ doc,
597
+ "/p:Response[@ID=$id]/a:Assertion#{subelt}",
598
+ { "p" => PROTOCOL, "a" => ASSERTION },
599
+ { 'id' => doc.signed_element_id }
600
+ )
601
+ node
602
+ end
603
+
604
+ # Extracts all the appearances that matchs the subelt (pattern)
605
+ # Search on any Assertion that is signed, or has a Response parent signed
606
+ # @param subelt [String] The XPath pattern
607
+ # @return [Array of REXML::Element] Return all matches
608
+ #
609
+ def xpath_from_signed_assertion(subelt=nil)
610
+ doc = decrypted_document.nil? ? document : decrypted_document
611
+ node = REXML::XPath.match(
612
+ doc,
613
+ "/p:Response/a:Assertion[@ID=$id]#{subelt}",
614
+ { "p" => PROTOCOL, "a" => ASSERTION },
615
+ { 'id' => doc.signed_element_id }
616
+ )
617
+ node.concat( REXML::XPath.match(
618
+ doc,
619
+ "/p:Response[@ID=$id]/a:Assertion#{subelt}",
620
+ { "p" => PROTOCOL, "a" => ASSERTION },
621
+ { 'id' => doc.signed_element_id }
622
+ ))
623
+ end
624
+
625
+ # Generates the decrypted_document
626
+ # @return [XMLSecurity::SignedDocument] The SAML Response with the assertion decrypted
627
+ #
628
+ def generate_decrypted_document
629
+ if settings.nil? || !settings.get_sp_key
630
+ validation_error('An EncryptedAssertion found and no SP private key found on the settings to decrypt it. Be sure you provided the :settings parameter at the initialize method')
631
+ end
632
+
633
+ # Marshal at Ruby 1.8.7 throw an Exception
634
+ if RUBY_VERSION < "1.9"
635
+ document_copy = XMLSecurity::SignedDocument.new(response, errors)
636
+ else
637
+ document_copy = Marshal.load(Marshal.dump(document))
638
+ end
639
+
640
+ decrypt_assertion_from_document(document_copy)
641
+ end
642
+
643
+ # Obtains a SAML Response with the EncryptedAssertion element decrypted
644
+ # @param document_copy [XMLSecurity::SignedDocument] A copy of the original SAML Response with the encrypted assertion
645
+ # @return [XMLSecurity::SignedDocument] The SAML Response with the assertion decrypted
646
+ #
647
+ def decrypt_assertion_from_document(document_copy)
648
+ response_node = REXML::XPath.first(
649
+ document_copy,
650
+ "/p:Response/",
651
+ { "p" => PROTOCOL }
652
+ )
653
+ encrypted_assertion_node = REXML::XPath.first(
654
+ document_copy,
655
+ "(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)",
656
+ { "p" => PROTOCOL, "a" => ASSERTION }
657
+ )
658
+ response_node.add(decrypt_assertion(encrypted_assertion_node))
659
+ encrypted_assertion_node.remove
660
+ XMLSecurity::SignedDocument.new(response_node.to_s)
661
+ end
662
+
663
+ # Checks if the SAML Response contains or not an EncryptedAssertion element
664
+ # @return [Boolean] True if the SAML Response contains an EncryptedAssertion element
665
+ #
666
+ def assertion_encrypted?
667
+ ! REXML::XPath.first(
668
+ document,
669
+ "(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)",
670
+ { "p" => PROTOCOL, "a" => ASSERTION }
671
+ ).nil?
672
+ end
673
+
674
+ # Decrypts an EncryptedAssertion element
675
+ # @param encrypted_assertion_node [REXML::Element] The EncryptedAssertion element
676
+ # @return [REXML::Document] The decrypted EncryptedAssertion element
677
+ #
678
+ def decrypt_assertion(encrypted_assertion_node)
679
+ decrypt_element(encrypted_assertion_node, /(.*<\/(saml2*:|)Assertion>)/m)
680
+ end
681
+
682
+ # Decrypts an EncryptedID element
683
+ # @param encryptedid_node [REXML::Element] The EncryptedID element
684
+ # @return [REXML::Document] The decrypted EncrypedtID element
685
+ #
686
+ def decrypt_nameid(encryptedid_node)
687
+ decrypt_element(encryptedid_node, /(.*<\/(saml2*:|)NameID>)/m)
688
+ end
689
+
690
+ # Decrypt an element
691
+ # @param encryptedid_node [REXML::Element] The encrypted element
692
+ # @return [REXML::Document] The decrypted element
693
+ #
694
+ def decrypt_element(encrypt_node, rgrex)
695
+ if settings.nil? || !settings.get_sp_key
696
+ return validation_error('An ' + encrypt_node.name + ' found and no SP private key found on the settings to decrypt it')
697
+ end
698
+
699
+ elem_plaintext = OneLogin::RubySaml::Utils.decrypt_data(encrypt_node, settings.get_sp_key)
700
+ # If we get some problematic noise in the plaintext after decrypting.
701
+ # This quick regexp parse will grab only the Element and discard the noise.
702
+ elem_plaintext = elem_plaintext.match(rgrex)[0]
703
+ # To avoid namespace errors if saml namespace is not defined at assertion_plaintext
704
+ # create a parent node first with the saml namespace defined
705
+ elem_plaintext = '<node xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">' + elem_plaintext + '</node>'
706
+ doc = REXML::Document.new(elem_plaintext)
707
+ doc.root[0]
708
+ end
709
+
710
+ # Parse the attribute of a given node in Time format
711
+ # @param node [REXML:Element] The node
712
+ # @param attribute [String] The attribute name
713
+ # @return [Time|nil] The parsed value
714
+ #
715
+ def parse_time(node, attribute)
716
+ if node && node.attributes[attribute]
717
+ Time.parse(node.attributes[attribute])
718
+ end
719
+ end
720
+ end
721
+ end
722
+ end