r-saml 1.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 (140) hide show
  1. checksums.yaml +7 -0
  2. data/.document +5 -0
  3. data/.gitignore +14 -0
  4. data/.travis.yml +23 -0
  5. data/Gemfile +6 -0
  6. data/LICENSE +19 -0
  7. data/README.md +584 -0
  8. data/Rakefile +27 -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 +165 -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 +735 -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 +368 -0
  43. data/r-saml.gemspec +64 -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 +1159 -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_without_reference_uri.xml.base64 +1 -0
  117. data/test/responses/response_wrapped.xml.base64 +150 -0
  118. data/test/responses/signed_message_encrypted_signed_assertion.xml.base64 +1 -0
  119. data/test/responses/signed_message_encrypted_unsigned_assertion.xml.base64 +1 -0
  120. data/test/responses/signed_nameid_in_atts.xml +47 -0
  121. data/test/responses/signed_unqual_nameid_in_atts.xml +47 -0
  122. data/test/responses/simple_saml_php.xml +71 -0
  123. data/test/responses/starfield_response.xml.base64 +1 -0
  124. data/test/responses/test_sign.xml +43 -0
  125. data/test/responses/unsigned_message_aes128_encrypted_signed_assertion.xml.base64 +1 -0
  126. data/test/responses/unsigned_message_aes192_encrypted_signed_assertion.xml.base64 +1 -0
  127. data/test/responses/unsigned_message_aes256_encrypted_signed_assertion.xml.base64 +1 -0
  128. data/test/responses/unsigned_message_des192_encrypted_signed_assertion.xml.base64 +1 -0
  129. data/test/responses/unsigned_message_encrypted_assertion_without_saml_namespace.xml.base64 +1 -0
  130. data/test/responses/unsigned_message_encrypted_signed_assertion.xml.base64 +1 -0
  131. data/test/responses/unsigned_message_encrypted_unsigned_assertion.xml.base64 +1 -0
  132. data/test/responses/valid_response.xml.base64 +1 -0
  133. data/test/saml_message_test.rb +56 -0
  134. data/test/settings_test.rb +218 -0
  135. data/test/slo_logoutrequest_test.rb +275 -0
  136. data/test/slo_logoutresponse_test.rb +185 -0
  137. data/test/test_helper.rb +257 -0
  138. data/test/utils_test.rb +145 -0
  139. data/test/xml_security_test.rb +328 -0
  140. metadata +421 -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,735 @@
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
+ # or skip the subject confirmation validation with the :skip_subject_confirmation option
41
+ def initialize(response, options = {})
42
+ @errors = []
43
+
44
+ raise ArgumentError.new("Response cannot be nil") if response.nil?
45
+ @options = options
46
+
47
+ @soft = true
48
+ if !options.empty? && !options[:settings].nil?
49
+ @settings = options[:settings]
50
+ if !options[:settings].soft.nil?
51
+ @soft = options[:settings].soft
52
+ end
53
+ end
54
+
55
+ @response = decode_raw_saml(response)
56
+ @document = XMLSecurity::SignedDocument.new(@response, @errors)
57
+
58
+ if assertion_encrypted?
59
+ @decrypted_document = generate_decrypted_document
60
+ end
61
+ end
62
+
63
+ # Append the cause to the errors array, and based on the value of soft, return false or raise
64
+ # an exception
65
+ def append_error(error_msg)
66
+ @errors << error_msg
67
+ return soft ? false : validation_error(error_msg)
68
+ end
69
+
70
+ # Reset the errors array
71
+ def reset_errors!
72
+ @errors = []
73
+ end
74
+
75
+ # Validates the SAML Response with the default values (soft = true)
76
+ # @return [Boolean] TRUE if the SAML Response is valid
77
+ #
78
+ def is_valid?
79
+ validate
80
+ end
81
+
82
+ # @return [String] the NameID provided by the SAML response from the IdP.
83
+ #
84
+ def name_id
85
+ @name_id ||= begin
86
+ encrypted_node = xpath_first_from_signed_assertion('/a:Subject/a:EncryptedID')
87
+ if encrypted_node
88
+ node = decrypt_nameid(encrypted_node)
89
+ else
90
+ node = xpath_first_from_signed_assertion('/a:Subject/a:NameID')
91
+ end
92
+ node.nil? ? nil : node.text
93
+ end
94
+ end
95
+
96
+ alias_method :nameid, :name_id
97
+
98
+
99
+ # Gets the SessionIndex from the AuthnStatement.
100
+ # Could be used to be stored in the local session in order
101
+ # to be used in a future Logout Request that the SP could
102
+ # send to the IdP, to set what specific session must be deleted
103
+ # @return [String] SessionIndex Value
104
+ #
105
+ def sessionindex
106
+ @sessionindex ||= begin
107
+ node = xpath_first_from_signed_assertion('/a:AuthnStatement')
108
+ node.nil? ? nil : node.attributes['SessionIndex']
109
+ end
110
+ end
111
+
112
+ # Gets the Attributes from the AttributeStatement element.
113
+ #
114
+ # All attributes can be iterated over +attributes.each+ or returned as array by +attributes.all+
115
+ # For backwards compatibility ruby-saml returns by default only the first value for a given attribute with
116
+ # attributes['name']
117
+ # To get all of the attributes, use:
118
+ # attributes.multi('name')
119
+ # Or turn off the compatibility:
120
+ # OneLogin::RubySaml::Attributes.single_value_compatibility = false
121
+ # Now this will return an array:
122
+ # attributes['name']
123
+ #
124
+ # @return [Attributes] OneLogin::RubySaml::Attributes enumerable collection.
125
+ #
126
+ def attributes
127
+ @attr_statements ||= begin
128
+ attributes = Attributes.new
129
+
130
+ stmt_element = xpath_first_from_signed_assertion('/a:AttributeStatement')
131
+ return attributes if stmt_element.nil?
132
+
133
+ stmt_element.elements.each do |attr_element|
134
+ name = attr_element.attributes["Name"]
135
+ values = attr_element.elements.collect{|e|
136
+ if (e.elements.nil? || e.elements.size == 0)
137
+ # SAMLCore requires that nil AttributeValues MUST contain xsi:nil XML attribute set to "true" or "1"
138
+ # otherwise the value is to be regarded as empty.
139
+ ["true", "1"].include?(e.attributes['xsi:nil']) ? nil : e.text.to_s
140
+ # explicitly support saml2:NameID with saml2:NameQualifier if supplied in attributes
141
+ # this is useful for allowing eduPersonTargetedId to be passed as an opaque identifier to use to
142
+ # identify the subject in an SP rather than email or other less opaque attributes
143
+ # NameQualifier, if present is prefixed with a "/" to the value
144
+ else
145
+ REXML::XPath.match(e,'a:NameID', { "a" => ASSERTION }).collect{|n|
146
+ (n.attributes['NameQualifier'] ? n.attributes['NameQualifier'] +"/" : '') + n.text.to_s
147
+ }
148
+ end
149
+ }
150
+
151
+ attributes.add(name, values.flatten)
152
+ end
153
+
154
+ attributes
155
+ end
156
+ end
157
+
158
+ # Gets the SessionNotOnOrAfter from the AuthnStatement.
159
+ # Could be used to set the local session expiration (expire at latest)
160
+ # @return [String] The SessionNotOnOrAfter value
161
+ #
162
+ def session_expires_at
163
+ @expires_at ||= begin
164
+ node = xpath_first_from_signed_assertion('/a:AuthnStatement')
165
+ node.nil? ? nil : parse_time(node, "SessionNotOnOrAfter")
166
+ end
167
+ end
168
+
169
+ # Checks if the Status has the "Success" code
170
+ # @return [Boolean] True if the StatusCode is Sucess
171
+ #
172
+ def success?
173
+ status_code == "urn:oasis:names:tc:SAML:2.0:status:Success"
174
+ end
175
+
176
+ # @return [String] StatusCode value from a SAML Response.
177
+ #
178
+ def status_code
179
+ @status_code ||= begin
180
+ node = REXML::XPath.first(
181
+ document,
182
+ "/p:Response/p:Status/p:StatusCode",
183
+ { "p" => PROTOCOL, "a" => ASSERTION }
184
+ )
185
+ node.attributes["Value"] if node && node.attributes
186
+ end
187
+ end
188
+
189
+ # @return [String] the StatusMessage value from a SAML Response.
190
+ #
191
+ def status_message
192
+ @status_message ||= begin
193
+ node = REXML::XPath.first(
194
+ document,
195
+ "/p:Response/p:Status/p:StatusMessage",
196
+ { "p" => PROTOCOL, "a" => ASSERTION }
197
+ )
198
+ node.text if node
199
+ end
200
+ end
201
+
202
+ # Gets the Condition Element of the SAML Response if exists.
203
+ # (returns the first node that matches the supplied xpath)
204
+ # @return [REXML::Element] Conditions Element if exists
205
+ #
206
+ def conditions
207
+ @conditions ||= xpath_first_from_signed_assertion('/a:Conditions')
208
+ end
209
+
210
+ # Gets the NotBefore Condition Element value.
211
+ # @return [Time] The NotBefore value in Time format
212
+ #
213
+ def not_before
214
+ @not_before ||= parse_time(conditions, "NotBefore")
215
+ end
216
+
217
+ # Gets the NotOnOrAfter Condition Element value.
218
+ # @return [Time] The NotOnOrAfter value in Time format
219
+ #
220
+ def not_on_or_after
221
+ @not_on_or_after ||= parse_time(conditions, "NotOnOrAfter")
222
+ end
223
+
224
+ # Gets the Issuers (from Response and Assertion).
225
+ # (returns the first node that matches the supplied xpath from the Response and from the Assertion)
226
+ # @return [Array] Array with the Issuers (REXML::Element)
227
+ #
228
+ def issuers
229
+ @issuers ||= begin
230
+ issuers = []
231
+ nodes = REXML::XPath.match(
232
+ document,
233
+ "/p:Response/a:Issuer",
234
+ { "p" => PROTOCOL, "a" => ASSERTION }
235
+ )
236
+ nodes += xpath_from_signed_assertion("/a:Issuer")
237
+ nodes.each do |node|
238
+ issuers << node.text if node.text
239
+ end
240
+ issuers.uniq
241
+ end
242
+ end
243
+
244
+ # @return [String|nil] The InResponseTo attribute from the SAML Response.
245
+ #
246
+ def in_response_to
247
+ @in_response_to ||= begin
248
+ node = REXML::XPath.first(
249
+ document,
250
+ "/p:Response",
251
+ { "p" => PROTOCOL }
252
+ )
253
+ node.nil? ? nil : node.attributes['InResponseTo']
254
+ end
255
+ end
256
+
257
+ # @return [Array] The Audience elements from the Contitions of the SAML Response.
258
+ #
259
+ def audiences
260
+ @audiences ||= begin
261
+ audiences = []
262
+ nodes = xpath_from_signed_assertion('/a:Conditions/a:AudienceRestriction/a:Audience')
263
+ nodes.each do |node|
264
+ if node && node.text
265
+ audiences << node.text
266
+ end
267
+ end
268
+ audiences
269
+ end
270
+ end
271
+
272
+ # returns the allowed clock drift on timing validation
273
+ # @return [Integer]
274
+ def allowed_clock_drift
275
+ return options[:allowed_clock_drift] || 0
276
+ end
277
+
278
+ private
279
+
280
+ # Validates the SAML Response (calls several validation methods)
281
+ # @return [Boolean] True if the SAML Response is valid, otherwise False if soft=True
282
+ # @raise [ValidationError] if soft == false and validation fails
283
+ #
284
+ def validate
285
+ reset_errors!
286
+
287
+ validate_response_state &&
288
+ validate_version &&
289
+ validate_id &&
290
+ validate_success_status &&
291
+ validate_num_assertion &&
292
+ validate_no_encrypted_attributes &&
293
+ validate_signed_elements &&
294
+ validate_structure &&
295
+ validate_in_response_to &&
296
+ validate_conditions &&
297
+ validate_audience &&
298
+ validate_issuer &&
299
+ validate_session_expiration &&
300
+ validate_subject_confirmation
301
+ end
302
+
303
+
304
+ # Validates the Status of the SAML Response
305
+ # @return [Boolean] True if the SAML Response contains a Success code, otherwise False if soft == false
306
+ # @raise [ValidationError] if soft == false and validation fails
307
+ #
308
+ def validate_success_status
309
+ return true if success?
310
+
311
+ error_msg = 'The status code of the Response was not Success'
312
+ status_error_msg = OneLogin::RubySaml::Utils.status_error_msg(error_msg, status_code, status_message)
313
+ append_error(status_error_msg)
314
+ end
315
+
316
+ # Validates the SAML Response against the specified schema.
317
+ # @return [Boolean] True if the XML is valid, otherwise False if soft=True
318
+ # @raise [ValidationError] if soft == false and validation fails
319
+ #
320
+ def validate_structure
321
+ unless valid_saml?(document, soft)
322
+ return append_error("Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd")
323
+ end
324
+
325
+ true
326
+ end
327
+
328
+ # Validates that the SAML Response provided in the initialization is not empty,
329
+ # also check that the setting and the IdP cert were also provided
330
+ # @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the response is invalid or not)
331
+ # @return [Boolean] True if the required info is found, otherwise False if soft=True
332
+ # @raise [ValidationError] if soft == false and validation fails
333
+ #
334
+ def validate_response_state(soft = true)
335
+ return append_error("Blank response") if response.nil? || response.empty?
336
+
337
+ return append_error("No settings on response") if settings.nil?
338
+
339
+ if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil?
340
+ return append_error("No fingerprint or certificate on settings")
341
+ end
342
+
343
+ true
344
+ end
345
+
346
+ # Validates that the SAML Response contains an ID
347
+ # If fails, the error is added to the errors array.
348
+ # @return [Boolean] True if the SAML Response contains an ID, otherwise returns False
349
+ #
350
+ def validate_id
351
+ unless id(document)
352
+ return append_error("Missing ID attribute on SAML Response")
353
+ end
354
+
355
+ true
356
+ end
357
+
358
+ # Validates the SAML version (2.0)
359
+ # If fails, the error is added to the errors array.
360
+ # @return [Boolean] True if the SAML Response is 2.0, otherwise returns False
361
+ #
362
+ def validate_version
363
+ unless version(document) == "2.0"
364
+ return append_error("Unsupported SAML version")
365
+ end
366
+
367
+ true
368
+ end
369
+
370
+ # Validates that the SAML Response only contains a single Assertion (encrypted or not).
371
+ # If fails, the error is added to the errors array.
372
+ # @return [Boolean] True if the SAML Response contains one unique Assertion, otherwise False
373
+ #
374
+ def validate_num_assertion
375
+ assertions = REXML::XPath.match(
376
+ document,
377
+ "//a:Assertion",
378
+ { "a" => ASSERTION }
379
+ )
380
+ encrypted_assertions = REXML::XPath.match(
381
+ document,
382
+ "//a:EncryptedAssertion",
383
+ { "a" => ASSERTION }
384
+ )
385
+
386
+ unless assertions.size + encrypted_assertions.size == 1
387
+ return append_error("SAML Response must contain 1 assertion")
388
+ end
389
+
390
+ true
391
+ end
392
+
393
+ # Validates that there are not EncryptedAttribute (not supported)
394
+ # If fails, the error is added to the errors array
395
+ # @return [Boolean] True if there are no EncryptedAttribute elements, otherwise False if soft=True
396
+ # @raise [ValidationError] if soft == false and validation fails
397
+ #
398
+ def validate_no_encrypted_attributes
399
+ nodes = xpath_from_signed_assertion("/a:AttributeStatement/a:EncryptedAttribute")
400
+ if nodes && nodes.length > 0
401
+ return append_error("There is an EncryptedAttribute in the Response and this SP not support them")
402
+ end
403
+
404
+ true
405
+ end
406
+
407
+
408
+ # Validates the Signed elements
409
+ # If fails, the error is added to the errors array
410
+ # @return [Boolean] True if there is 1 or 2 Elements signed in the SAML Response
411
+ # an are a Response or an Assertion Element, otherwise False if soft=True
412
+ #
413
+ def validate_signed_elements
414
+ signature_nodes = REXML::XPath.match(
415
+ decrypted_document.nil? ? document : decrypted_document,
416
+ "//ds:Signature",
417
+ {"ds"=>DSIG}
418
+ )
419
+ signed_elements = []
420
+ signature_nodes.each do |signature_node|
421
+ signed_element = signature_node.parent.name
422
+ if signed_element != 'Response' && signed_element != 'Assertion'
423
+ return append_error("Found an unexpected Signature Element. SAML Response rejected")
424
+ end
425
+ signed_elements << signed_element
426
+ end
427
+
428
+ unless signature_nodes.length < 3 && !signed_elements.empty?
429
+ return append_error("Found an unexpected number of Signature Element. SAML Response rejected")
430
+ end
431
+
432
+ true
433
+ end
434
+
435
+ # Validates if the provided request_id match the inResponseTo value.
436
+ # If fails, the error is added to the errors array
437
+ # @return [Boolean] True if there is no request_id or it match, otherwise False if soft=True
438
+ # @raise [ValidationError] if soft == false and validation fails
439
+ #
440
+ def validate_in_response_to
441
+ return true unless options.has_key? :matches_request_id
442
+ return true if options[:matches_request_id].nil? || options[:matches_request_id].empty?
443
+ return true unless options[:matches_request_id] != in_response_to
444
+
445
+ 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]}"
446
+ append_error(error_msg)
447
+ end
448
+
449
+ # Validates the Audience, (If the Audience match the Service Provider EntityID)
450
+ # If fails, the error is added to the errors array
451
+ # @return [Boolean] True if there is an Audience Element that match the Service Provider EntityID, otherwise False if soft=True
452
+ # @raise [ValidationError] if soft == false and validation fails
453
+ #
454
+ def validate_audience
455
+ return true if audiences.empty? || settings.issuer.nil? || settings.issuer.empty?
456
+
457
+ unless audiences.include? settings.issuer
458
+ error_msg = "#{settings.issuer} is not a valid audience for this Response"
459
+ return append_error(error_msg)
460
+ end
461
+
462
+ true
463
+ end
464
+
465
+ # Validates the Conditions. (If the response was initialized with the :skip_conditions option, this validation is skipped,
466
+ # If the response was initialized with the :allowed_clock_drift option, the timing validations are relaxed by the allowed_clock_drift value)
467
+ # @return [Boolean] True if satisfies the conditions, otherwise False if soft=True
468
+ # @raise [ValidationError] if soft == false and validation fails
469
+ #
470
+ def validate_conditions
471
+ return true if conditions.nil?
472
+ return true if options[:skip_conditions]
473
+
474
+ now = Time.now.utc
475
+
476
+ if not_before && (now + allowed_clock_drift) < not_before
477
+ error_msg = "Current time is earlier than NotBefore condition #{(now + allowed_clock_drift)} < #{not_before})"
478
+ return append_error(error_msg)
479
+ end
480
+
481
+ if not_on_or_after && now >= (not_on_or_after + allowed_clock_drift)
482
+ error_msg = "Current time is on or after NotOnOrAfter condition (#{now} >= #{not_on_or_after + allowed_clock_drift})"
483
+ return append_error(error_msg)
484
+ end
485
+
486
+ true
487
+ end
488
+
489
+ # Validates the Issuer (Of the SAML Response and the SAML Assertion)
490
+ # @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the response is invalid or not)
491
+ # @return [Boolean] True if the Issuer matchs the IdP entityId, otherwise False if soft=True
492
+ # @raise [ValidationError] if soft == false and validation fails
493
+ #
494
+ def validate_issuer
495
+ return true if settings.idp_entity_id.nil?
496
+
497
+ issuers.each do |issuer|
498
+ unless URI.parse(issuer) == URI.parse(settings.idp_entity_id)
499
+ error_msg = "Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>"
500
+ return append_error(error_msg)
501
+ end
502
+ end
503
+
504
+ true
505
+ end
506
+
507
+ # Validates that the Session haven't expired (If the response was initialized with the :allowed_clock_drift option,
508
+ # this time validation is relaxed by the allowed_clock_drift value)
509
+ # If fails, the error is added to the errors array
510
+ # @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the response is invalid or not)
511
+ # @return [Boolean] True if the SessionNotOnOrAfter of the AttributeStatement is valid, otherwise (when expired) False if soft=True
512
+ # @raise [ValidationError] if soft == false and validation fails
513
+ #
514
+ def validate_session_expiration(soft = true)
515
+ return true if session_expires_at.nil?
516
+
517
+ now = Time.now.utc
518
+ unless (session_expires_at + allowed_clock_drift) > now
519
+ error_msg = "The attributes have expired, based on the SessionNotOnOrAfter of the AttributeStatement of this Response"
520
+ return append_error(error_msg)
521
+ end
522
+
523
+ true
524
+ end
525
+
526
+ # Validates if exists valid SubjectConfirmation (If the response was initialized with the :allowed_clock_drift option,
527
+ # timimg validation are relaxed by the allowed_clock_drift value. If the response was initialized with the
528
+ # :skip_subject_confirmation option, this validation is skipped)
529
+ # If fails, the error is added to the errors array
530
+ # @return [Boolean] True if exists a valid SubjectConfirmation, otherwise False if soft=True
531
+ # @raise [ValidationError] if soft == false and validation fails
532
+ #
533
+ def validate_subject_confirmation
534
+ return true if options[:skip_subject_confirmation]
535
+ valid_subject_confirmation = false
536
+
537
+ subject_confirmation_nodes = xpath_from_signed_assertion('/a:Subject/a:SubjectConfirmation')
538
+
539
+ now = Time.now.utc
540
+ subject_confirmation_nodes.each do |subject_confirmation|
541
+ if subject_confirmation.attributes.include? "Method" and subject_confirmation.attributes['Method'] != 'urn:oasis:names:tc:SAML:2.0:cm:bearer'
542
+ next
543
+ end
544
+
545
+ confirmation_data_node = REXML::XPath.first(
546
+ subject_confirmation,
547
+ 'a:SubjectConfirmationData',
548
+ { "a" => ASSERTION }
549
+ )
550
+
551
+ next unless confirmation_data_node
552
+
553
+ attrs = confirmation_data_node.attributes
554
+ next if (attrs.include? "InResponseTo" and attrs['InResponseTo'] != in_response_to) ||
555
+ (attrs.include? "NotOnOrAfter" and (parse_time(confirmation_data_node, "NotOnOrAfter") + allowed_clock_drift) <= now) ||
556
+ (attrs.include? "NotBefore" and parse_time(confirmation_data_node, "NotBefore") > (now + allowed_clock_drift))
557
+
558
+ valid_subject_confirmation = true
559
+ break
560
+ end
561
+
562
+ if !valid_subject_confirmation
563
+ error_msg = "A valid SubjectConfirmation was not found on this Response"
564
+ return append_error(error_msg)
565
+ end
566
+
567
+ true
568
+ end
569
+
570
+ # Validates the Signature
571
+ # @return [Boolean] True if not contains a Signature or if the Signature is valid, otherwise False if soft=True
572
+ # @raise [ValidationError] if soft == false and validation fails
573
+ #
574
+ def validate_signature
575
+ fingerprint = settings.get_fingerprint
576
+
577
+ # If the response contains the signature, and the assertion was encrypted, validate the original SAML Response
578
+ # otherwise, review if the decrypted assertion contains a signature
579
+ response_signed = REXML::XPath.first(
580
+ document,
581
+ "/p:Response[@ID=$id]",
582
+ { "p" => PROTOCOL, "ds" => DSIG },
583
+ { 'id' => document.signed_element_id }
584
+ )
585
+ doc = (response_signed || decrypted_document.nil?) ? document : decrypted_document
586
+
587
+ unless fingerprint && doc.validate_document(fingerprint, :fingerprint_alg => settings.idp_cert_fingerprint_algorithm)
588
+ error_msg = "Invalid Signature on SAML Response"
589
+ return append_error(error_msg)
590
+ end
591
+
592
+ true
593
+ end
594
+
595
+ # Extracts the first appearance that matchs the subelt (pattern)
596
+ # Search on any Assertion that is signed, or has a Response parent signed
597
+ # @param subelt [String] The XPath pattern
598
+ # @return [REXML::Element | nil] If any matches, return the Element
599
+ #
600
+ def xpath_first_from_signed_assertion(subelt=nil)
601
+ doc = decrypted_document.nil? ? document : decrypted_document
602
+ node = REXML::XPath.first(
603
+ doc,
604
+ "/p:Response/a:Assertion[@ID=$id]#{subelt}",
605
+ { "p" => PROTOCOL, "a" => ASSERTION },
606
+ { 'id' => doc.signed_element_id }
607
+ )
608
+ node ||= REXML::XPath.first(
609
+ doc,
610
+ "/p:Response[@ID=$id]/a:Assertion#{subelt}",
611
+ { "p" => PROTOCOL, "a" => ASSERTION },
612
+ { 'id' => doc.signed_element_id }
613
+ )
614
+ node
615
+ end
616
+
617
+ # Extracts all the appearances that matchs the subelt (pattern)
618
+ # Search on any Assertion that is signed, or has a Response parent signed
619
+ # @param subelt [String] The XPath pattern
620
+ # @return [Array of REXML::Element] Return all matches
621
+ #
622
+ def xpath_from_signed_assertion(subelt=nil)
623
+ doc = decrypted_document.nil? ? document : decrypted_document
624
+ node = REXML::XPath.match(
625
+ doc,
626
+ "/p:Response/a:Assertion[@ID=$id]#{subelt}",
627
+ { "p" => PROTOCOL, "a" => ASSERTION },
628
+ { 'id' => doc.signed_element_id }
629
+ )
630
+ node.concat( REXML::XPath.match(
631
+ doc,
632
+ "/p:Response[@ID=$id]/a:Assertion#{subelt}",
633
+ { "p" => PROTOCOL, "a" => ASSERTION },
634
+ { 'id' => doc.signed_element_id }
635
+ ))
636
+ end
637
+
638
+ # Generates the decrypted_document
639
+ # @return [XMLSecurity::SignedDocument] The SAML Response with the assertion decrypted
640
+ #
641
+ def generate_decrypted_document
642
+ if settings.nil? || !settings.get_sp_key
643
+ 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')
644
+ end
645
+
646
+ # Marshal at Ruby 1.8.7 throw an Exception
647
+ if RUBY_VERSION < "1.9"
648
+ document_copy = XMLSecurity::SignedDocument.new(response, errors)
649
+ else
650
+ document_copy = Marshal.load(Marshal.dump(document))
651
+ end
652
+
653
+ decrypt_assertion_from_document(document_copy)
654
+ end
655
+
656
+ # Obtains a SAML Response with the EncryptedAssertion element decrypted
657
+ # @param document_copy [XMLSecurity::SignedDocument] A copy of the original SAML Response with the encrypted assertion
658
+ # @return [XMLSecurity::SignedDocument] The SAML Response with the assertion decrypted
659
+ #
660
+ def decrypt_assertion_from_document(document_copy)
661
+ response_node = REXML::XPath.first(
662
+ document_copy,
663
+ "/p:Response/",
664
+ { "p" => PROTOCOL }
665
+ )
666
+ encrypted_assertion_node = REXML::XPath.first(
667
+ document_copy,
668
+ "(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)",
669
+ { "p" => PROTOCOL, "a" => ASSERTION }
670
+ )
671
+ response_node.add(decrypt_assertion(encrypted_assertion_node))
672
+ encrypted_assertion_node.remove
673
+ XMLSecurity::SignedDocument.new(response_node.to_s)
674
+ end
675
+
676
+ # Checks if the SAML Response contains or not an EncryptedAssertion element
677
+ # @return [Boolean] True if the SAML Response contains an EncryptedAssertion element
678
+ #
679
+ def assertion_encrypted?
680
+ ! REXML::XPath.first(
681
+ document,
682
+ "(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)",
683
+ { "p" => PROTOCOL, "a" => ASSERTION }
684
+ ).nil?
685
+ end
686
+
687
+ # Decrypts an EncryptedAssertion element
688
+ # @param encrypted_assertion_node [REXML::Element] The EncryptedAssertion element
689
+ # @return [REXML::Document] The decrypted EncryptedAssertion element
690
+ #
691
+ def decrypt_assertion(encrypted_assertion_node)
692
+ decrypt_element(encrypted_assertion_node, /(.*<\/(saml2*:|)Assertion>)/m)
693
+ end
694
+
695
+ # Decrypts an EncryptedID element
696
+ # @param encryptedid_node [REXML::Element] The EncryptedID element
697
+ # @return [REXML::Document] The decrypted EncrypedtID element
698
+ #
699
+ def decrypt_nameid(encryptedid_node)
700
+ decrypt_element(encryptedid_node, /(.*<\/(saml2*:|)NameID>)/m)
701
+ end
702
+
703
+ # Decrypt an element
704
+ # @param encryptedid_node [REXML::Element] The encrypted element
705
+ # @return [REXML::Document] The decrypted element
706
+ #
707
+ def decrypt_element(encrypt_node, rgrex)
708
+ if settings.nil? || !settings.get_sp_key
709
+ return validation_error('An ' + encrypt_node.name + ' found and no SP private key found on the settings to decrypt it')
710
+ end
711
+
712
+ elem_plaintext = OneLogin::RubySaml::Utils.decrypt_data(encrypt_node, settings.get_sp_key)
713
+ # If we get some problematic noise in the plaintext after decrypting.
714
+ # This quick regexp parse will grab only the Element and discard the noise.
715
+ elem_plaintext = elem_plaintext.match(rgrex)[0]
716
+ # To avoid namespace errors if saml namespace is not defined at assertion_plaintext
717
+ # create a parent node first with the saml namespace defined
718
+ elem_plaintext = '<node xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">' + elem_plaintext + '</node>'
719
+ doc = REXML::Document.new(elem_plaintext)
720
+ doc.root[0]
721
+ end
722
+
723
+ # Parse the attribute of a given node in Time format
724
+ # @param node [REXML:Element] The node
725
+ # @param attribute [String] The attribute name
726
+ # @return [Time|nil] The parsed value
727
+ #
728
+ def parse_time(node, attribute)
729
+ if node && node.attributes[attribute]
730
+ Time.parse(node.attributes[attribute])
731
+ end
732
+ end
733
+ end
734
+ end
735
+ end