ruby-saml 1.12.4 → 1.18.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.
- checksums.yaml +4 -4
- data/.github/FUNDING.yml +3 -0
- data/.github/workflows/test.yml +29 -2
- data/{changelog.md → CHANGELOG.md} +64 -15
- data/LICENSE +2 -1
- data/README.md +425 -233
- data/UPGRADING.md +158 -0
- data/lib/onelogin/ruby-saml/authrequest.rb +9 -11
- data/lib/onelogin/ruby-saml/idp_metadata_parser.rb +115 -84
- data/lib/onelogin/ruby-saml/logoutrequest.rb +9 -9
- data/lib/onelogin/ruby-saml/logoutresponse.rb +2 -2
- data/lib/onelogin/ruby-saml/metadata.rb +75 -42
- data/lib/onelogin/ruby-saml/response.rb +130 -70
- data/lib/onelogin/ruby-saml/saml_message.rb +16 -19
- data/lib/onelogin/ruby-saml/settings.rb +214 -110
- data/lib/onelogin/ruby-saml/slo_logoutrequest.rb +51 -37
- data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +9 -9
- data/lib/onelogin/ruby-saml/utils.rb +129 -46
- data/lib/onelogin/ruby-saml/version.rb +1 -1
- data/lib/xml_security.rb +81 -48
- data/ruby-saml.gemspec +40 -14
- metadata +29 -32
- data/.travis.yml +0 -48
|
@@ -43,7 +43,7 @@ module OneLogin
|
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
@options = options
|
|
46
|
-
@response = decode_raw_saml(response)
|
|
46
|
+
@response = decode_raw_saml(response, settings)
|
|
47
47
|
@document = XMLSecurity::SignedDocument.new(@response)
|
|
48
48
|
end
|
|
49
49
|
|
|
@@ -213,7 +213,7 @@ module OneLogin
|
|
|
213
213
|
return true unless options.has_key? :get_params
|
|
214
214
|
return true unless options[:get_params].has_key? 'Signature'
|
|
215
215
|
|
|
216
|
-
options[:raw_get_params] = OneLogin::RubySaml::Utils.prepare_raw_get_params(options[:raw_get_params], options[:get_params])
|
|
216
|
+
options[:raw_get_params] = OneLogin::RubySaml::Utils.prepare_raw_get_params(options[:raw_get_params], options[:get_params], settings.security[:lowercase_url_encoding])
|
|
217
217
|
|
|
218
218
|
if options[:get_params]['SigAlg'].nil? && !options[:raw_get_params]['SigAlg'].nil?
|
|
219
219
|
options[:get_params]['SigAlg'] = CGI.unescape(options[:raw_get_params]['SigAlg'])
|
|
@@ -21,53 +21,61 @@ module OneLogin
|
|
|
21
21
|
#
|
|
22
22
|
def generate(settings, pretty_print=false, valid_until=nil, cache_duration=nil)
|
|
23
23
|
meta_doc = XMLSecurity::Document.new
|
|
24
|
+
add_xml_declaration(meta_doc)
|
|
25
|
+
root = add_root_element(meta_doc, settings, valid_until, cache_duration)
|
|
26
|
+
sp_sso = add_sp_sso_element(root, settings)
|
|
27
|
+
add_sp_certificates(sp_sso, settings)
|
|
28
|
+
add_sp_service_elements(sp_sso, settings)
|
|
29
|
+
add_extras(root, settings)
|
|
30
|
+
embed_signature(meta_doc, settings)
|
|
31
|
+
output_xml(meta_doc, pretty_print)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
protected
|
|
35
|
+
|
|
36
|
+
def add_xml_declaration(meta_doc)
|
|
37
|
+
meta_doc << REXML::XMLDecl.new('1.0', 'UTF-8')
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def add_root_element(meta_doc, settings, valid_until, cache_duration)
|
|
24
41
|
namespaces = {
|
|
25
42
|
"xmlns:md" => "urn:oasis:names:tc:SAML:2.0:metadata"
|
|
26
43
|
}
|
|
44
|
+
|
|
27
45
|
if settings.attribute_consuming_service.configured?
|
|
28
46
|
namespaces["xmlns:saml"] = "urn:oasis:names:tc:SAML:2.0:assertion"
|
|
29
47
|
end
|
|
30
|
-
|
|
31
|
-
|
|
48
|
+
|
|
49
|
+
root = meta_doc.add_element("md:EntityDescriptor", namespaces)
|
|
50
|
+
root.attributes["ID"] = OneLogin::RubySaml::Utils.uuid
|
|
51
|
+
root.attributes["entityID"] = settings.sp_entity_id if settings.sp_entity_id
|
|
52
|
+
root.attributes["validUntil"] = valid_until.utc.strftime('%Y-%m-%dT%H:%M:%SZ') if valid_until
|
|
53
|
+
root.attributes["cacheDuration"] = "PT" + cache_duration.to_s + "S" if cache_duration
|
|
54
|
+
root
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def add_sp_sso_element(root, settings)
|
|
58
|
+
root.add_element "md:SPSSODescriptor", {
|
|
32
59
|
"protocolSupportEnumeration" => "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
33
60
|
"AuthnRequestsSigned" => settings.security[:authn_requests_signed],
|
|
34
61
|
"WantAssertionsSigned" => settings.security[:want_assertions_signed],
|
|
35
62
|
}
|
|
63
|
+
end
|
|
36
64
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
cert_new = settings.get_sp_cert_new
|
|
41
|
-
|
|
42
|
-
for sp_cert in [cert, cert_new]
|
|
43
|
-
if sp_cert
|
|
44
|
-
cert_text = Base64.encode64(sp_cert.to_der).gsub("\n", '')
|
|
45
|
-
kd = sp_sso.add_element "md:KeyDescriptor", { "use" => "signing" }
|
|
46
|
-
ki = kd.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"}
|
|
47
|
-
xd = ki.add_element "ds:X509Data"
|
|
48
|
-
xc = xd.add_element "ds:X509Certificate"
|
|
49
|
-
xc.text = cert_text
|
|
50
|
-
|
|
51
|
-
if settings.security[:want_assertions_encrypted]
|
|
52
|
-
kd2 = sp_sso.add_element "md:KeyDescriptor", { "use" => "encryption" }
|
|
53
|
-
ki2 = kd2.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"}
|
|
54
|
-
xd2 = ki2.add_element "ds:X509Data"
|
|
55
|
-
xc2 = xd2.add_element "ds:X509Certificate"
|
|
56
|
-
xc2.text = cert_text
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
65
|
+
# Add KeyDescriptor elements for SP certificates.
|
|
66
|
+
def add_sp_certificates(sp_sso, settings)
|
|
67
|
+
certs = settings.get_sp_certs
|
|
60
68
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if valid_until
|
|
66
|
-
root.attributes["validUntil"] = valid_until.strftime('%Y-%m-%dT%H:%M:%S%z')
|
|
67
|
-
end
|
|
68
|
-
if cache_duration
|
|
69
|
-
root.attributes["cacheDuration"] = "PT" + cache_duration.to_s + "S"
|
|
69
|
+
certs[:signing].each { |cert, _| add_sp_cert_element(sp_sso, cert, :signing) }
|
|
70
|
+
|
|
71
|
+
if settings.security[:want_assertions_encrypted]
|
|
72
|
+
certs[:encryption].each { |cert, _| add_sp_cert_element(sp_sso, cert, :encryption) }
|
|
70
73
|
end
|
|
74
|
+
|
|
75
|
+
sp_sso
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def add_sp_service_elements(sp_sso, settings)
|
|
71
79
|
if settings.single_logout_service_url
|
|
72
80
|
sp_sso.add_element "md:SingleLogoutService", {
|
|
73
81
|
"Binding" => settings.single_logout_service_binding,
|
|
@@ -75,10 +83,12 @@ module OneLogin
|
|
|
75
83
|
"ResponseLocation" => settings.single_logout_service_url
|
|
76
84
|
}
|
|
77
85
|
end
|
|
86
|
+
|
|
78
87
|
if settings.name_identifier_format
|
|
79
88
|
nameid = sp_sso.add_element "md:NameIDFormat"
|
|
80
89
|
nameid.text = settings.name_identifier_format
|
|
81
90
|
end
|
|
91
|
+
|
|
82
92
|
if settings.assertion_consumer_service_url
|
|
83
93
|
sp_sso.add_element "md:AssertionConsumerService", {
|
|
84
94
|
"Binding" => settings.assertion_consumer_service_binding,
|
|
@@ -117,15 +127,26 @@ module OneLogin
|
|
|
117
127
|
# <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"/>
|
|
118
128
|
# <md:XACMLAuthzDecisionQueryDescriptor WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"/>
|
|
119
129
|
|
|
120
|
-
|
|
130
|
+
sp_sso
|
|
131
|
+
end
|
|
121
132
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
133
|
+
# can be overridden in subclass
|
|
134
|
+
def add_extras(root, _settings)
|
|
135
|
+
root
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def embed_signature(meta_doc, settings)
|
|
139
|
+
return unless settings.security[:metadata_signed]
|
|
140
|
+
|
|
141
|
+
cert, private_key = settings.get_sp_signing_pair
|
|
142
|
+
return unless private_key && cert
|
|
143
|
+
|
|
144
|
+
meta_doc.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def output_xml(meta_doc, pretty_print)
|
|
148
|
+
ret = ''.dup
|
|
127
149
|
|
|
128
|
-
ret = ""
|
|
129
150
|
# pretty print the XML so IdP administrators can easily see what the SP supports
|
|
130
151
|
if pretty_print
|
|
131
152
|
meta_doc.write(ret, 1)
|
|
@@ -133,7 +154,19 @@ module OneLogin
|
|
|
133
154
|
ret = meta_doc.to_s
|
|
134
155
|
end
|
|
135
156
|
|
|
136
|
-
|
|
157
|
+
ret
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
def add_sp_cert_element(sp_sso, cert, use)
|
|
163
|
+
return unless cert
|
|
164
|
+
cert_text = Base64.encode64(cert.to_der).gsub("\n", '')
|
|
165
|
+
kd = sp_sso.add_element "md:KeyDescriptor", { "use" => use.to_s }
|
|
166
|
+
ki = kd.add_element "ds:KeyInfo", { "xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#" }
|
|
167
|
+
xd = ki.add_element "ds:X509Data"
|
|
168
|
+
xc = xd.add_element "ds:X509Certificate"
|
|
169
|
+
xc.text = cert_text
|
|
137
170
|
end
|
|
138
171
|
end
|
|
139
172
|
end
|
|
@@ -17,6 +17,10 @@ module OneLogin
|
|
|
17
17
|
PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
|
|
18
18
|
DSIG = "http://www.w3.org/2000/09/xmldsig#"
|
|
19
19
|
XENC = "http://www.w3.org/2001/04/xmlenc#"
|
|
20
|
+
SAML_NAMESPACES = {
|
|
21
|
+
"p" => PROTOCOL,
|
|
22
|
+
"a" => ASSERTION
|
|
23
|
+
}.freeze
|
|
20
24
|
|
|
21
25
|
# TODO: Settings should probably be initialized too... WDYT?
|
|
22
26
|
|
|
@@ -63,7 +67,7 @@ module OneLogin
|
|
|
63
67
|
end
|
|
64
68
|
end
|
|
65
69
|
|
|
66
|
-
@response = decode_raw_saml(response)
|
|
70
|
+
@response = decode_raw_saml(response, settings)
|
|
67
71
|
@document = XMLSecurity::SignedDocument.new(@response, @errors)
|
|
68
72
|
|
|
69
73
|
if assertion_encrypted?
|
|
@@ -198,6 +202,27 @@ module OneLogin
|
|
|
198
202
|
end
|
|
199
203
|
end
|
|
200
204
|
|
|
205
|
+
# Gets the AuthnInstant from the AuthnStatement.
|
|
206
|
+
# Could be used to require re-authentication if a long time has passed
|
|
207
|
+
# since the last user authentication.
|
|
208
|
+
# @return [String] AuthnInstant value
|
|
209
|
+
#
|
|
210
|
+
def authn_instant
|
|
211
|
+
@authn_instant ||= begin
|
|
212
|
+
node = xpath_first_from_signed_assertion('/a:AuthnStatement')
|
|
213
|
+
node.nil? ? nil : node.attributes['AuthnInstant']
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Gets the AuthnContextClassRef from the AuthnStatement
|
|
218
|
+
# Could be used to require re-authentication if the assertion
|
|
219
|
+
# did not met the requested authentication context class.
|
|
220
|
+
# @return [String] AuthnContextClassRef value
|
|
221
|
+
#
|
|
222
|
+
def authn_context_class_ref
|
|
223
|
+
@authn_context_class_ref ||= Utils.element_text(xpath_first_from_signed_assertion('/a:AuthnStatement/a:AuthnContext/a:AuthnContextClassRef'))
|
|
224
|
+
end
|
|
225
|
+
|
|
201
226
|
# Checks if the Status has the "Success" code
|
|
202
227
|
# @return [Boolean] True if the StatusCode is Sucess
|
|
203
228
|
#
|
|
@@ -227,11 +252,10 @@ module OneLogin
|
|
|
227
252
|
statuses = nodes.collect do |inner_node|
|
|
228
253
|
inner_node.attributes["Value"]
|
|
229
254
|
end
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
code = "#{code} | #{extra_code}"
|
|
233
|
-
end
|
|
255
|
+
|
|
256
|
+
code = [code, statuses].flatten.join(" | ")
|
|
234
257
|
end
|
|
258
|
+
|
|
235
259
|
code
|
|
236
260
|
end
|
|
237
261
|
end
|
|
@@ -283,7 +307,7 @@ module OneLogin
|
|
|
283
307
|
issuer_response_nodes = REXML::XPath.match(
|
|
284
308
|
document,
|
|
285
309
|
"/p:Response/a:Issuer",
|
|
286
|
-
|
|
310
|
+
SAML_NAMESPACES
|
|
287
311
|
)
|
|
288
312
|
|
|
289
313
|
unless issuer_response_nodes.size == 1
|
|
@@ -338,9 +362,9 @@ module OneLogin
|
|
|
338
362
|
end
|
|
339
363
|
|
|
340
364
|
# returns the allowed clock drift on timing validation
|
|
341
|
-
# @return [
|
|
365
|
+
# @return [Float]
|
|
342
366
|
def allowed_clock_drift
|
|
343
|
-
|
|
367
|
+
options[:allowed_clock_drift].to_f.abs + Float::EPSILON
|
|
344
368
|
end
|
|
345
369
|
|
|
346
370
|
# Checks if the SAML Response contains or not an EncryptedAssertion element
|
|
@@ -350,7 +374,7 @@ module OneLogin
|
|
|
350
374
|
! REXML::XPath.first(
|
|
351
375
|
document,
|
|
352
376
|
"(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)",
|
|
353
|
-
|
|
377
|
+
SAML_NAMESPACES
|
|
354
378
|
).nil?
|
|
355
379
|
end
|
|
356
380
|
|
|
@@ -377,14 +401,13 @@ module OneLogin
|
|
|
377
401
|
return false unless validate_response_state
|
|
378
402
|
|
|
379
403
|
validations = [
|
|
380
|
-
:validate_response_state,
|
|
381
404
|
:validate_version,
|
|
382
405
|
:validate_id,
|
|
383
406
|
:validate_success_status,
|
|
384
407
|
:validate_num_assertion,
|
|
385
|
-
:validate_no_duplicated_attributes,
|
|
386
408
|
:validate_signed_elements,
|
|
387
409
|
:validate_structure,
|
|
410
|
+
:validate_no_duplicated_attributes,
|
|
388
411
|
:validate_in_response_to,
|
|
389
412
|
:validate_one_conditions,
|
|
390
413
|
:validate_conditions,
|
|
@@ -425,6 +448,7 @@ module OneLogin
|
|
|
425
448
|
#
|
|
426
449
|
def validate_structure
|
|
427
450
|
structure_error_msg = "Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd"
|
|
451
|
+
|
|
428
452
|
check_malformed_doc = check_malformed_doc_enabled?
|
|
429
453
|
unless valid_saml?(document, soft, check_malformed_doc)
|
|
430
454
|
return append_error(structure_error_msg)
|
|
@@ -616,7 +640,12 @@ module OneLogin
|
|
|
616
640
|
#
|
|
617
641
|
def validate_audience
|
|
618
642
|
return true if options[:skip_audience]
|
|
619
|
-
return true if
|
|
643
|
+
return true if settings.sp_entity_id.nil? || settings.sp_entity_id.empty?
|
|
644
|
+
|
|
645
|
+
if audiences.empty?
|
|
646
|
+
return true unless settings.security[:strict_audience_validation]
|
|
647
|
+
return append_error("Invalid Audiences. The <AudienceRestriction> element contained only empty <Audience> elements. Expected audience #{settings.sp_entity_id}.")
|
|
648
|
+
end
|
|
620
649
|
|
|
621
650
|
unless audiences.include? settings.sp_entity_id
|
|
622
651
|
s = audiences.count > 1 ? 's' : '';
|
|
@@ -695,13 +724,13 @@ module OneLogin
|
|
|
695
724
|
|
|
696
725
|
now = Time.now.utc
|
|
697
726
|
|
|
698
|
-
if not_before &&
|
|
699
|
-
error_msg = "Current time is earlier than NotBefore condition (#{
|
|
727
|
+
if not_before && now < (not_before - allowed_clock_drift)
|
|
728
|
+
error_msg = "Current time is earlier than NotBefore condition (#{now} < #{not_before}#{" - #{allowed_clock_drift.ceil}s" if allowed_clock_drift > 0})"
|
|
700
729
|
return append_error(error_msg)
|
|
701
730
|
end
|
|
702
731
|
|
|
703
|
-
if not_on_or_after && now >= (
|
|
704
|
-
error_msg = "Current time is on or after NotOnOrAfter condition (#{now} >= #{
|
|
732
|
+
if not_on_or_after && now >= (not_on_or_after + allowed_clock_drift)
|
|
733
|
+
error_msg = "Current time is on or after NotOnOrAfter condition (#{now} >= #{not_on_or_after}#{" + #{allowed_clock_drift.ceil}s" if allowed_clock_drift > 0})"
|
|
705
734
|
return append_error(error_msg)
|
|
706
735
|
end
|
|
707
736
|
|
|
@@ -739,11 +768,11 @@ module OneLogin
|
|
|
739
768
|
# @return [Boolean] True if the SessionNotOnOrAfter of the AuthnStatement is valid, otherwise (when expired) False if soft=True
|
|
740
769
|
# @raise [ValidationError] if soft == false and validation fails
|
|
741
770
|
#
|
|
742
|
-
def validate_session_expiration
|
|
771
|
+
def validate_session_expiration
|
|
743
772
|
return true if session_expires_at.nil?
|
|
744
773
|
|
|
745
774
|
now = Time.now.utc
|
|
746
|
-
unless (session_expires_at + allowed_clock_drift)
|
|
775
|
+
unless now < (session_expires_at + allowed_clock_drift)
|
|
747
776
|
error_msg = "The attributes have expired, based on the SessionNotOnOrAfter of the AuthnStatement of this Response"
|
|
748
777
|
return append_error(error_msg)
|
|
749
778
|
end
|
|
@@ -781,8 +810,8 @@ module OneLogin
|
|
|
781
810
|
|
|
782
811
|
attrs = confirmation_data_node.attributes
|
|
783
812
|
next if (attrs.include? "InResponseTo" and attrs['InResponseTo'] != in_response_to) ||
|
|
784
|
-
(attrs.include? "
|
|
785
|
-
(attrs.include? "
|
|
813
|
+
(attrs.include? "NotBefore" and now < (parse_time(confirmation_data_node, "NotBefore") - allowed_clock_drift)) ||
|
|
814
|
+
(attrs.include? "NotOnOrAfter" and now >= (parse_time(confirmation_data_node, "NotOnOrAfter") + allowed_clock_drift)) ||
|
|
786
815
|
(attrs.include? "Recipient" and !options[:skip_recipient_check] and settings and attrs['Recipient'] != settings.assertion_consumer_service_url)
|
|
787
816
|
|
|
788
817
|
valid_subject_confirmation = true
|
|
@@ -810,7 +839,7 @@ module OneLogin
|
|
|
810
839
|
|
|
811
840
|
unless settings.sp_entity_id.nil? || settings.sp_entity_id.empty? || name_id_spnamequalifier.nil? || name_id_spnamequalifier.empty?
|
|
812
841
|
if name_id_spnamequalifier != settings.sp_entity_id
|
|
813
|
-
return append_error("
|
|
842
|
+
return append_error("SPNameQualifier value does not match the SP entityID value.")
|
|
814
843
|
end
|
|
815
844
|
end
|
|
816
845
|
end
|
|
@@ -818,6 +847,25 @@ module OneLogin
|
|
|
818
847
|
true
|
|
819
848
|
end
|
|
820
849
|
|
|
850
|
+
def doc_to_validate
|
|
851
|
+
# If the response contains the signature, and the assertion was encrypted, validate the original SAML Response
|
|
852
|
+
# otherwise, review if the decrypted assertion contains a signature
|
|
853
|
+
sig_elements = REXML::XPath.match(
|
|
854
|
+
document,
|
|
855
|
+
"/p:Response[@ID=$id]/ds:Signature",
|
|
856
|
+
{ "p" => PROTOCOL, "ds" => DSIG },
|
|
857
|
+
{ 'id' => document.signed_element_id }
|
|
858
|
+
)
|
|
859
|
+
|
|
860
|
+
use_original = sig_elements.size == 1 || decrypted_document.nil?
|
|
861
|
+
doc = use_original ? document : decrypted_document
|
|
862
|
+
if !doc.processed
|
|
863
|
+
doc.cache_referenced_xml(@soft, check_malformed_doc_enabled?)
|
|
864
|
+
end
|
|
865
|
+
|
|
866
|
+
return doc
|
|
867
|
+
end
|
|
868
|
+
|
|
821
869
|
# Validates the Signature
|
|
822
870
|
# @return [Boolean] True if not contains a Signature or if the Signature is valid, otherwise False if soft=True
|
|
823
871
|
# @raise [ValidationError] if soft == false and validation fails
|
|
@@ -825,8 +873,8 @@ module OneLogin
|
|
|
825
873
|
def validate_signature
|
|
826
874
|
error_msg = "Invalid Signature on SAML Response"
|
|
827
875
|
|
|
828
|
-
|
|
829
|
-
|
|
876
|
+
doc = doc_to_validate
|
|
877
|
+
|
|
830
878
|
sig_elements = REXML::XPath.match(
|
|
831
879
|
document,
|
|
832
880
|
"/p:Response[@ID=$id]/ds:Signature",
|
|
@@ -834,15 +882,12 @@ module OneLogin
|
|
|
834
882
|
{ 'id' => document.signed_element_id }
|
|
835
883
|
)
|
|
836
884
|
|
|
837
|
-
|
|
838
|
-
doc = use_original ? document : decrypted_document
|
|
839
|
-
|
|
840
|
-
# Check signature nodes
|
|
885
|
+
# Check signature node inside assertion
|
|
841
886
|
if sig_elements.nil? || sig_elements.size == 0
|
|
842
887
|
sig_elements = REXML::XPath.match(
|
|
843
888
|
doc,
|
|
844
889
|
"/p:Response/a:Assertion[@ID=$id]/ds:Signature",
|
|
845
|
-
{"
|
|
890
|
+
SAML_NAMESPACES.merge({"ds"=>DSIG}),
|
|
846
891
|
{ 'id' => doc.signed_element_id }
|
|
847
892
|
)
|
|
848
893
|
end
|
|
@@ -866,8 +911,6 @@ module OneLogin
|
|
|
866
911
|
fingerprint = settings.get_fingerprint
|
|
867
912
|
opts[:cert] = idp_cert
|
|
868
913
|
|
|
869
|
-
check_malformed_doc = check_malformed_doc_enabled?
|
|
870
|
-
opts[:check_malformed_doc] = check_malformed_doc
|
|
871
914
|
if fingerprint && doc.validate_document(fingerprint, @soft, opts)
|
|
872
915
|
if settings.security[:check_idp_cert_expiration]
|
|
873
916
|
if OneLogin::RubySaml::Utils.is_cert_expired(idp_cert)
|
|
@@ -882,7 +925,7 @@ module OneLogin
|
|
|
882
925
|
valid = false
|
|
883
926
|
expired = false
|
|
884
927
|
idp_certs[:signing].each do |idp_cert|
|
|
885
|
-
valid = doc.validate_document_with_cert(idp_cert, true
|
|
928
|
+
valid = doc.validate_document_with_cert(idp_cert, true)
|
|
886
929
|
if valid
|
|
887
930
|
if settings.security[:check_idp_cert_expiration]
|
|
888
931
|
if OneLogin::RubySaml::Utils.is_cert_expired(idp_cert)
|
|
@@ -915,31 +958,54 @@ module OneLogin
|
|
|
915
958
|
begin
|
|
916
959
|
encrypted_node = xpath_first_from_signed_assertion('/a:Subject/a:EncryptedID')
|
|
917
960
|
if encrypted_node
|
|
918
|
-
|
|
961
|
+
decrypt_nameid(encrypted_node)
|
|
919
962
|
else
|
|
920
|
-
|
|
963
|
+
xpath_first_from_signed_assertion('/a:Subject/a:NameID')
|
|
921
964
|
end
|
|
922
965
|
end
|
|
923
966
|
end
|
|
924
967
|
|
|
968
|
+
def get_cached_signed_assertion
|
|
969
|
+
xml = doc_to_validate.referenced_xml
|
|
970
|
+
empty_doc = REXML::Document.new
|
|
971
|
+
|
|
972
|
+
return empty_doc if xml.nil? # when no signature/reference is found, return empty document
|
|
973
|
+
|
|
974
|
+
root = REXML::Document.new(xml).root
|
|
975
|
+
|
|
976
|
+
if root.attributes["ID"] != doc_to_validate.signed_element_id
|
|
977
|
+
return empty_doc
|
|
978
|
+
end
|
|
979
|
+
|
|
980
|
+
assertion = empty_doc
|
|
981
|
+
if root.name == "Response"
|
|
982
|
+
if REXML::XPath.first(root, "a:Assertion", {"a" => ASSERTION})
|
|
983
|
+
assertion = REXML::XPath.first(root, "a:Assertion", {"a" => ASSERTION})
|
|
984
|
+
elsif REXML::XPath.first(root, "a:EncryptedAssertion", {"a" => ASSERTION})
|
|
985
|
+
assertion = decrypt_assertion(REXML::XPath.first(root, "a:EncryptedAssertion", {"a" => ASSERTION}))
|
|
986
|
+
end
|
|
987
|
+
elsif root.name == "Assertion"
|
|
988
|
+
assertion = root
|
|
989
|
+
end
|
|
990
|
+
|
|
991
|
+
assertion
|
|
992
|
+
end
|
|
993
|
+
|
|
994
|
+
def signed_assertion
|
|
995
|
+
@signed_assertion ||= get_cached_signed_assertion
|
|
996
|
+
end
|
|
997
|
+
|
|
925
998
|
# Extracts the first appearance that matchs the subelt (pattern)
|
|
926
999
|
# Search on any Assertion that is signed, or has a Response parent signed
|
|
927
1000
|
# @param subelt [String] The XPath pattern
|
|
928
1001
|
# @return [REXML::Element | nil] If any matches, return the Element
|
|
929
1002
|
#
|
|
930
1003
|
def xpath_first_from_signed_assertion(subelt=nil)
|
|
931
|
-
doc =
|
|
1004
|
+
doc = signed_assertion
|
|
932
1005
|
node = REXML::XPath.first(
|
|
933
1006
|
doc,
|
|
934
|
-
"
|
|
935
|
-
|
|
936
|
-
{ 'id' => doc.signed_element_id }
|
|
937
|
-
)
|
|
938
|
-
node ||= REXML::XPath.first(
|
|
939
|
-
doc,
|
|
940
|
-
"/p:Response[@ID=$id]/a:Assertion#{subelt}",
|
|
941
|
-
{ "p" => PROTOCOL, "a" => ASSERTION },
|
|
942
|
-
{ 'id' => doc.signed_element_id }
|
|
1007
|
+
"./#{subelt}",
|
|
1008
|
+
SAML_NAMESPACES
|
|
943
1009
|
)
|
|
944
1010
|
node
|
|
945
1011
|
end
|
|
@@ -950,26 +1016,20 @@ module OneLogin
|
|
|
950
1016
|
# @return [Array of REXML::Element] Return all matches
|
|
951
1017
|
#
|
|
952
1018
|
def xpath_from_signed_assertion(subelt=nil)
|
|
953
|
-
doc =
|
|
1019
|
+
doc = signed_assertion
|
|
954
1020
|
node = REXML::XPath.match(
|
|
955
1021
|
doc,
|
|
956
|
-
"
|
|
957
|
-
|
|
958
|
-
{ 'id' => doc.signed_element_id }
|
|
1022
|
+
"./#{subelt}",
|
|
1023
|
+
SAML_NAMESPACES
|
|
959
1024
|
)
|
|
960
|
-
node
|
|
961
|
-
doc,
|
|
962
|
-
"/p:Response[@ID=$id]/a:Assertion#{subelt}",
|
|
963
|
-
{ "p" => PROTOCOL, "a" => ASSERTION },
|
|
964
|
-
{ 'id' => doc.signed_element_id }
|
|
965
|
-
))
|
|
1025
|
+
node
|
|
966
1026
|
end
|
|
967
1027
|
|
|
968
1028
|
# Generates the decrypted_document
|
|
969
1029
|
# @return [XMLSecurity::SignedDocument] The SAML Response with the assertion decrypted
|
|
970
1030
|
#
|
|
971
1031
|
def generate_decrypted_document
|
|
972
|
-
if settings.nil? ||
|
|
1032
|
+
if settings.nil? || settings.get_sp_decryption_keys.empty?
|
|
973
1033
|
raise ValidationError.new('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')
|
|
974
1034
|
end
|
|
975
1035
|
|
|
@@ -996,7 +1056,7 @@ module OneLogin
|
|
|
996
1056
|
encrypted_assertion_node = REXML::XPath.first(
|
|
997
1057
|
document_copy,
|
|
998
1058
|
"(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)",
|
|
999
|
-
|
|
1059
|
+
SAML_NAMESPACES
|
|
1000
1060
|
)
|
|
1001
1061
|
response_node.add(decrypt_assertion(encrypted_assertion_node))
|
|
1002
1062
|
encrypted_assertion_node.remove
|
|
@@ -1012,42 +1072,42 @@ module OneLogin
|
|
|
1012
1072
|
end
|
|
1013
1073
|
|
|
1014
1074
|
# Decrypts an EncryptedID element
|
|
1015
|
-
# @param
|
|
1075
|
+
# @param encrypted_id_node [REXML::Element] The EncryptedID element
|
|
1016
1076
|
# @return [REXML::Document] The decrypted EncrypedtID element
|
|
1017
1077
|
#
|
|
1018
|
-
def decrypt_nameid(
|
|
1019
|
-
decrypt_element(
|
|
1078
|
+
def decrypt_nameid(encrypted_id_node)
|
|
1079
|
+
decrypt_element(encrypted_id_node, /(.*<\/(\w+:)?NameID>)/m)
|
|
1020
1080
|
end
|
|
1021
1081
|
|
|
1022
|
-
# Decrypts an
|
|
1023
|
-
# @param
|
|
1024
|
-
# @return [REXML::Document] The decrypted
|
|
1082
|
+
# Decrypts an EncryptedAttribute element
|
|
1083
|
+
# @param encrypted_attribute_node [REXML::Element] The EncryptedAttribute element
|
|
1084
|
+
# @return [REXML::Document] The decrypted EncryptedAttribute element
|
|
1025
1085
|
#
|
|
1026
|
-
def decrypt_attribute(
|
|
1027
|
-
decrypt_element(
|
|
1086
|
+
def decrypt_attribute(encrypted_attribute_node)
|
|
1087
|
+
decrypt_element(encrypted_attribute_node, /(.*<\/(\w+:)?Attribute>)/m)
|
|
1028
1088
|
end
|
|
1029
1089
|
|
|
1030
1090
|
# Decrypt an element
|
|
1031
|
-
# @param
|
|
1032
|
-
# @param
|
|
1091
|
+
# @param encrypt_node [REXML::Element] The encrypted element
|
|
1092
|
+
# @param regexp [Regexp] The regular expression to extract the decrypted data
|
|
1033
1093
|
# @return [REXML::Document] The decrypted element
|
|
1034
1094
|
#
|
|
1035
|
-
def decrypt_element(encrypt_node,
|
|
1036
|
-
if settings.nil? ||
|
|
1095
|
+
def decrypt_element(encrypt_node, regexp)
|
|
1096
|
+
if settings.nil? || settings.get_sp_decryption_keys.empty?
|
|
1037
1097
|
raise ValidationError.new('An ' + encrypt_node.name + ' found and no SP private key found on the settings to decrypt it')
|
|
1038
1098
|
end
|
|
1039
1099
|
|
|
1040
|
-
|
|
1041
1100
|
if encrypt_node.name == 'EncryptedAttribute'
|
|
1042
1101
|
node_header = '<node xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">'
|
|
1043
1102
|
else
|
|
1044
1103
|
node_header = '<node xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">'
|
|
1045
1104
|
end
|
|
1046
1105
|
|
|
1047
|
-
elem_plaintext = OneLogin::RubySaml::Utils.
|
|
1106
|
+
elem_plaintext = OneLogin::RubySaml::Utils.decrypt_multi(encrypt_node, settings.get_sp_decryption_keys)
|
|
1107
|
+
|
|
1048
1108
|
# If we get some problematic noise in the plaintext after decrypting.
|
|
1049
1109
|
# This quick regexp parse will grab only the Element and discard the noise.
|
|
1050
|
-
elem_plaintext = elem_plaintext.match(
|
|
1110
|
+
elem_plaintext = elem_plaintext.match(regexp)[0]
|
|
1051
1111
|
|
|
1052
1112
|
# To avoid namespace errors if saml namespace is not defined
|
|
1053
1113
|
# create a parent node first with the namespace defined
|