ruby-saml 0.8.12 → 0.8.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ruby-saml might be problematic. Click here for more details.
- checksums.yaml +7 -7
- data/lib/onelogin/ruby-saml/logoutrequest.rb +2 -1
- data/lib/onelogin/ruby-saml/logoutresponse.rb +9 -51
- data/lib/onelogin/ruby-saml/response.rb +133 -21
- data/lib/onelogin/ruby-saml/settings.rb +28 -10
- data/lib/onelogin/ruby-saml/slo_logoutrequest.rb +101 -0
- data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +12 -9
- data/lib/onelogin/ruby-saml/utils.rb +92 -0
- data/lib/onelogin/ruby-saml/version.rb +1 -1
- data/lib/ruby-saml.rb +1 -0
- data/lib/xml_security.rb +222 -86
- data/test/certificates/certificate.der +0 -0
- data/test/certificates/formatted_certificate +14 -0
- data/test/certificates/formatted_chained_certificate +42 -0
- data/test/certificates/formatted_private_key +12 -0
- data/test/certificates/formatted_rsa_private_key +12 -0
- data/test/certificates/invalid_certificate1 +1 -0
- data/test/certificates/invalid_certificate2 +1 -0
- data/test/certificates/invalid_certificate3 +12 -0
- data/test/certificates/invalid_chained_certificate1 +1 -0
- data/test/certificates/invalid_private_key1 +1 -0
- data/test/certificates/invalid_private_key2 +1 -0
- data/test/certificates/invalid_private_key3 +10 -0
- data/test/certificates/invalid_rsa_private_key1 +1 -0
- data/test/certificates/invalid_rsa_private_key2 +1 -0
- data/test/certificates/invalid_rsa_private_key3 +10 -0
- data/test/certificates/ruby-saml-2.crt +15 -0
- data/test/logoutresponse_test.rb +2 -16
- data/test/requests/logoutrequest_fixtures.rb +47 -0
- data/test/response_test.rb +227 -15
- data/test/responses/adfs_response_xmlns.xml +45 -0
- data/test/responses/invalids/invalid_issuer_assertion.xml.base64 +1 -0
- data/test/responses/invalids/invalid_issuer_message.xml.base64 +1 -0
- data/test/responses/invalids/multiple_signed.xml.base64 +1 -0
- data/test/responses/invalids/no_signature.xml.base64 +1 -0
- data/test/responses/invalids/response_with_concealed_signed_assertion.xml +51 -0
- data/test/responses/invalids/response_with_doubled_signed_assertion.xml +49 -0
- data/test/responses/invalids/signature_wrapping_attack.xml.base64 +1 -0
- data/test/responses/logoutresponse_fixtures.rb +4 -4
- data/test/responses/response_with_signed_assertion_3.xml +30 -0
- data/test/responses/response_with_signed_message_and_assertion.xml +34 -0
- data/test/responses/response_with_undefined_recipient.xml.base64 +1 -0
- data/test/responses/valid_response_without_x509certificate.xml.base64 +1 -0
- data/test/settings_test.rb +106 -0
- data/test/slo_logoutrequest_test.rb +66 -0
- data/test/slo_logoutresponse_test.rb +8 -0
- data/test/test_helper.rb +62 -30
- data/test/utils_test.rb +191 -1
- data/test/xml_security_test.rb +329 -36
- metadata +109 -45
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
5
|
-
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1ec15a6a64795cd0b10b796d6aef230a7d7d439c
|
4
|
+
data.tar.gz: a07ddee9fb7bfe9ca2f20cde2c9cadfd5bbac121
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 013b1a3b9b2eb015253dcc4992a1d8be73a3dce996271d701375914e3e7f3e64f6eff3094af1ade43fc807ce452093d05c071cf79b5b2cc8b63d3041688f9344
|
7
|
+
data.tar.gz: ed258b192c0cbd0b5c589183ecacd9591dad637783430130a9dfd97b6c68c1062520cf91255ed997adf693f1c7f344d84cf0184a821202c8c8ea85509c069864
|
@@ -114,7 +114,8 @@ module OneLogin
|
|
114
114
|
|
115
115
|
if settings.name_identifier_value
|
116
116
|
name_id = root.add_element "saml:NameID", { "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion" }
|
117
|
-
|
117
|
+
nameid.attributes['NameQualifier'] = settings.idp_name_qualifier if settings.idp_name_qualifier
|
118
|
+
nameid.attributes['SPNameQualifier'] = settings.sp_name_qualifier if settings.sp_name_qualifier
|
118
119
|
name_id.attributes['Format'] = settings.name_identifier_format if settings.name_identifier_format
|
119
120
|
name_id.text = settings.name_identifier_value
|
120
121
|
end
|
@@ -1,7 +1,5 @@
|
|
1
1
|
require "xml_security"
|
2
2
|
require "time"
|
3
|
-
require "base64"
|
4
|
-
require "zlib"
|
5
3
|
|
6
4
|
module OneLogin
|
7
5
|
module RubySaml
|
@@ -30,8 +28,8 @@ module OneLogin
|
|
30
28
|
self.settings = settings
|
31
29
|
|
32
30
|
@options = options
|
33
|
-
@response =
|
34
|
-
@document = XMLSecurity::SignedDocument.new(response)
|
31
|
+
@response = OneLogin::RubySaml::Utils.decode_raw_saml(response)
|
32
|
+
@document = XMLSecurity::SignedDocument.new(@response)
|
35
33
|
end
|
36
34
|
|
37
35
|
def validate!
|
@@ -39,7 +37,7 @@ module OneLogin
|
|
39
37
|
end
|
40
38
|
|
41
39
|
def validate(soft = true)
|
42
|
-
return false unless
|
40
|
+
return false unless validate_structure(soft)
|
43
41
|
|
44
42
|
valid_in_response_to?(soft) && valid_issuer?(soft) && success?(soft)
|
45
43
|
end
|
@@ -53,7 +51,7 @@ module OneLogin
|
|
53
51
|
|
54
52
|
def in_response_to
|
55
53
|
@in_response_to ||= begin
|
56
|
-
node = REXML::XPath.first(document, "/p:LogoutResponse", { "p" => PROTOCOL
|
54
|
+
node = REXML::XPath.first(document, "/p:LogoutResponse", { "p" => PROTOCOL })
|
57
55
|
node.nil? ? nil : node.attributes['InResponseTo']
|
58
56
|
end
|
59
57
|
end
|
@@ -61,7 +59,6 @@ module OneLogin
|
|
61
59
|
def issuer
|
62
60
|
@issuer ||= begin
|
63
61
|
node = REXML::XPath.first(document, "/p:LogoutResponse/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
|
64
|
-
node ||= REXML::XPath.first(document, "/p:LogoutResponse/a:Assertion/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
|
65
62
|
Utils.element_text(node)
|
66
63
|
end
|
67
64
|
end
|
@@ -75,28 +72,7 @@ module OneLogin
|
|
75
72
|
|
76
73
|
private
|
77
74
|
|
78
|
-
def
|
79
|
-
Base64.decode64(encoded)
|
80
|
-
end
|
81
|
-
|
82
|
-
def inflate(deflated)
|
83
|
-
zlib = Zlib::Inflate.new(-Zlib::MAX_WBITS)
|
84
|
-
zlib.inflate(deflated)
|
85
|
-
end
|
86
|
-
|
87
|
-
def decode_raw_response(response)
|
88
|
-
if response =~ /^</
|
89
|
-
return response
|
90
|
-
elsif (decoded = decode(response)) =~ /^</
|
91
|
-
return decoded
|
92
|
-
elsif (inflated = inflate(decoded)) =~ /^</
|
93
|
-
return inflated
|
94
|
-
end
|
95
|
-
|
96
|
-
raise "Couldn't decode SAMLResponse"
|
97
|
-
end
|
98
|
-
|
99
|
-
def valid_saml?(soft = true)
|
75
|
+
def validate_structure(soft = true)
|
100
76
|
Dir.chdir(File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'schemas'))) do
|
101
77
|
@schema = Nokogiri::XML::Schema(IO.read('saml20protocol_schema.xsd'))
|
102
78
|
@xml = Nokogiri::XML(self.document.to_s)
|
@@ -108,26 +84,6 @@ module OneLogin
|
|
108
84
|
end
|
109
85
|
end
|
110
86
|
|
111
|
-
def valid_state?(soft = true)
|
112
|
-
if response.empty?
|
113
|
-
return soft ? false : validation_error("Blank response")
|
114
|
-
end
|
115
|
-
|
116
|
-
if settings.nil?
|
117
|
-
return soft ? false : validation_error("No settings on response")
|
118
|
-
end
|
119
|
-
|
120
|
-
if settings.sp_entity_id.nil?
|
121
|
-
return soft ? false : validation_error("No sp_entity_id in settings")
|
122
|
-
end
|
123
|
-
|
124
|
-
if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil?
|
125
|
-
return soft ? false : validation_error("No fingerprint or certificate on settings")
|
126
|
-
end
|
127
|
-
|
128
|
-
true
|
129
|
-
end
|
130
|
-
|
131
87
|
def valid_in_response_to?(soft = true)
|
132
88
|
return true unless self.options.has_key? :matches_request_id
|
133
89
|
|
@@ -139,8 +95,10 @@ module OneLogin
|
|
139
95
|
end
|
140
96
|
|
141
97
|
def valid_issuer?(soft = true)
|
142
|
-
|
143
|
-
|
98
|
+
return true if settings.nil? || settings.idp_entity_id.nil? || issuer.nil?
|
99
|
+
|
100
|
+
unless OneLogin::RubySaml::Utils.uri_match?(issuer, settings.idp_entity_id)
|
101
|
+
return soft ? false : validation_error("Doesn't match the issuer, expected: <#{self.settings.idp_entity_id}>, but was: <#{issuer}>")
|
144
102
|
end
|
145
103
|
true
|
146
104
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
require "xml_security"
|
2
2
|
require "time"
|
3
3
|
require "nokogiri"
|
4
|
+
require "onelogin/ruby-saml/utils"
|
4
5
|
require 'onelogin/ruby-saml/attributes'
|
5
6
|
|
6
7
|
# Only supports SAML 2.0
|
@@ -22,7 +23,7 @@ module OneLogin
|
|
22
23
|
def initialize(response, options = {})
|
23
24
|
raise ArgumentError.new("Response cannot be nil") if response.nil?
|
24
25
|
@options = options
|
25
|
-
@response =
|
26
|
+
@response = OneLogin::RubySaml::Utils.decode_raw_saml(response)
|
26
27
|
@document = XMLSecurity::SignedDocument.new(@response)
|
27
28
|
end
|
28
29
|
|
@@ -34,16 +35,48 @@ module OneLogin
|
|
34
35
|
validate(false)
|
35
36
|
end
|
36
37
|
|
37
|
-
|
38
|
-
def name_id
|
38
|
+
def name_id_node
|
39
39
|
@name_id ||= begin
|
40
|
-
|
41
|
-
Utils.element_text(node)
|
40
|
+
xpath_first_from_signed_assertion('/a:Subject/a:NameID')
|
42
41
|
end
|
43
42
|
end
|
44
43
|
|
44
|
+
# The value of the user identifier as designated by the initialization request response
|
45
|
+
def name_id
|
46
|
+
@name_id ||= Utils.element_text(name_id_node)
|
47
|
+
end
|
48
|
+
|
45
49
|
alias nameid name_id
|
46
50
|
|
51
|
+
# @return [String] the NameID Format provided by the SAML response from the IdP.
|
52
|
+
#
|
53
|
+
def name_id_format
|
54
|
+
@name_id_format ||=
|
55
|
+
if name_id_node && name_id_node.attribute("Format")
|
56
|
+
name_id_node.attribute("Format").value
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
alias_method :nameid_format, :name_id_format
|
61
|
+
|
62
|
+
# @return [String] the NameID SPNameQualifier provided by the SAML response from the IdP.
|
63
|
+
#
|
64
|
+
def name_id_spnamequalifier
|
65
|
+
@name_id_spnamequalifier ||=
|
66
|
+
if name_id_node && name_id_node.attribute("SPNameQualifier")
|
67
|
+
name_id_node.attribute("SPNameQualifier").value
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# @return [String] the NameID NameQualifier provided by the SAML response from the IdP.
|
72
|
+
#
|
73
|
+
def name_id_namequalifier
|
74
|
+
@name_id_namequalifier ||=
|
75
|
+
if name_id_node && name_id_node.attribute("NameQualifier")
|
76
|
+
name_id_node.attribute("NameQualifier").value
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
47
80
|
def sessionindex
|
48
81
|
@sessionindex ||= begin
|
49
82
|
node = xpath_first_from_signed_assertion('/a:AuthnStatement')
|
@@ -133,6 +166,34 @@ module OneLogin
|
|
133
166
|
end
|
134
167
|
end
|
135
168
|
|
169
|
+
# Gets the Issuers (from Response and Assertion).
|
170
|
+
# (returns the first node that matches the supplied xpath from the Response and from the Assertion)
|
171
|
+
# @return [Array] Array with the Issuers (REXML::Element)
|
172
|
+
#
|
173
|
+
def issuers
|
174
|
+
@issuers ||= begin
|
175
|
+
issuer_response_nodes = REXML::XPath.match(
|
176
|
+
document,
|
177
|
+
"/p:Response/a:Issuer",
|
178
|
+
{ "p" => PROTOCOL, "a" => ASSERTION }
|
179
|
+
)
|
180
|
+
|
181
|
+
unless issuer_response_nodes.size == 1
|
182
|
+
error_msg = "Issuer of the Response not found or multiple."
|
183
|
+
raise ValidationError.new(error_msg)
|
184
|
+
end
|
185
|
+
|
186
|
+
issuer_assertion_nodes = xpath_from_signed_assertion("/a:Issuer")
|
187
|
+
unless issuer_assertion_nodes.size == 1
|
188
|
+
error_msg = "Issuer of the Assertion not found or multiple."
|
189
|
+
raise ValidationError.new(error_msg)
|
190
|
+
end
|
191
|
+
|
192
|
+
nodes = issuer_response_nodes + issuer_assertion_nodes
|
193
|
+
nodes.map { |node| Utils.element_text(node) }.compact.uniq
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
136
197
|
# @return [Array] The Audience elements from the Contitions of the SAML Response.
|
137
198
|
#
|
138
199
|
def audiences
|
@@ -156,7 +217,8 @@ module OneLogin
|
|
156
217
|
validate_response_state(soft) &&
|
157
218
|
validate_conditions(soft) &&
|
158
219
|
validate_audience(soft) &&
|
159
|
-
|
220
|
+
validate_issuer(soft) &&
|
221
|
+
validate_signature(soft) &&
|
160
222
|
success?
|
161
223
|
end
|
162
224
|
|
@@ -224,7 +286,6 @@ module OneLogin
|
|
224
286
|
|
225
287
|
if verified_seis.include?(sei)
|
226
288
|
return soft ? false : validation_error("Duplicated Reference URI. SAML Response rejected")
|
227
|
-
return append_error("Duplicated Reference URI. SAML Response rejected")
|
228
289
|
end
|
229
290
|
|
230
291
|
verified_seis.push(sei)
|
@@ -373,15 +434,6 @@ module OneLogin
|
|
373
434
|
))
|
374
435
|
end
|
375
436
|
|
376
|
-
def get_fingerprint
|
377
|
-
if settings.idp_cert
|
378
|
-
cert = OpenSSL::X509::Certificate.new(settings.idp_cert)
|
379
|
-
Digest::SHA1.hexdigest(cert.to_der).upcase.scan(/../).join(":")
|
380
|
-
else
|
381
|
-
settings.idp_cert_fingerprint
|
382
|
-
end
|
383
|
-
end
|
384
|
-
|
385
437
|
def validate_conditions(soft = true)
|
386
438
|
return true if conditions.nil?
|
387
439
|
return true if options[:skip_conditions]
|
@@ -399,17 +451,76 @@ module OneLogin
|
|
399
451
|
true
|
400
452
|
end
|
401
453
|
|
454
|
+
def validate_issuer(soft = true)
|
455
|
+
return true if settings.idp_entity_id.nil?
|
456
|
+
|
457
|
+
begin
|
458
|
+
obtained_issuers = issuers
|
459
|
+
rescue ValidationError => e
|
460
|
+
return soft ? false : validation_error("Error while extracting issuers")
|
461
|
+
end
|
462
|
+
|
463
|
+
obtained_issuers.each do |issuer|
|
464
|
+
unless OneLogin::RubySaml::Utils.uri_match?(issuer, settings.idp_entity_id)
|
465
|
+
error_msg = "Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>"
|
466
|
+
return soft ? false : validation_error(error_msg)
|
467
|
+
end
|
468
|
+
end
|
469
|
+
|
470
|
+
true
|
471
|
+
end
|
472
|
+
|
473
|
+
def validate_signature(soft = true)
|
474
|
+
error_msg = "Invalid Signature on SAML Response"
|
475
|
+
|
476
|
+
sig_elements = REXML::XPath.match(
|
477
|
+
document,
|
478
|
+
"/p:Response[@ID=$id]/ds:Signature]",
|
479
|
+
{ "p" => PROTOCOL, "ds" => DSIG },
|
480
|
+
{ 'id' => document.signed_element_id }
|
481
|
+
)
|
482
|
+
|
483
|
+
# Check signature nodes
|
484
|
+
if sig_elements.nil? || sig_elements.size == 0
|
485
|
+
sig_elements = REXML::XPath.match(
|
486
|
+
document,
|
487
|
+
"/p:Response/a:Assertion[@ID=$id]/ds:Signature",
|
488
|
+
{"p" => PROTOCOL, "a" => ASSERTION, "ds"=>DSIG},
|
489
|
+
{ 'id' => document.signed_element_id }
|
490
|
+
)
|
491
|
+
end
|
492
|
+
|
493
|
+
if sig_elements.size != 1
|
494
|
+
if sig_elements.size == 0
|
495
|
+
error_msg += ". Signed element id ##{doc.signed_element_id} is not found"
|
496
|
+
else
|
497
|
+
error_msg += ". Signed element id ##{doc.signed_element_id} is found more than once"
|
498
|
+
end
|
499
|
+
return soft ? false : validation_error(error_msg)
|
500
|
+
end
|
501
|
+
|
502
|
+
opts = {}
|
503
|
+
opts[:fingerprint_alg] = OpenSSL::Digest::SHA1.new
|
504
|
+
opts[:cert] = settings.get_idp_cert
|
505
|
+
fingerprint = settings.get_fingerprint
|
506
|
+
|
507
|
+
unless fingerprint
|
508
|
+
return soft ? false : validation_error("No fingerprint or certificate on settings")
|
509
|
+
end
|
510
|
+
|
511
|
+
unless document.validate_document(fingerprint, soft, opts)
|
512
|
+
return soft ? false : validation_error(error_msg)
|
513
|
+
end
|
514
|
+
|
515
|
+
true
|
516
|
+
end
|
517
|
+
|
402
518
|
def parse_time(node, attribute)
|
403
519
|
if node && node.attributes[attribute]
|
404
520
|
Time.parse(node.attributes[attribute])
|
405
521
|
end
|
406
522
|
end
|
407
523
|
|
408
|
-
# Validates the Audience, (If the Audience match the Service Provider EntityID)
|
409
|
-
# If fails, the error is added to the errors array
|
410
|
-
# @return [Boolean] True if there is an Audience Element that match the Service Provider EntityID, otherwise False if soft=True
|
411
|
-
# @raise [ValidationError] if soft == false and validation fails
|
412
|
-
#
|
413
524
|
def validate_audience(soft = true)
|
414
525
|
return true if audiences.empty? || settings.sp_entity_id.nil? || settings.sp_entity_id.empty?
|
415
526
|
|
@@ -421,6 +532,7 @@ module OneLogin
|
|
421
532
|
|
422
533
|
true
|
423
534
|
end
|
535
|
+
|
424
536
|
end
|
425
537
|
end
|
426
538
|
end
|
@@ -27,16 +27,17 @@ module OneLogin
|
|
27
27
|
attr_accessor :idp_cert_fingerprint
|
28
28
|
attr_accessor :idp_cert
|
29
29
|
attr_accessor :idp_slo_target_url
|
30
|
+
attr_accessor :idp_entity_id
|
30
31
|
#sp data
|
31
32
|
attr_accessor :sp_entity_id
|
32
33
|
attr_accessor :assertion_consumer_service_url
|
33
34
|
attr_accessor :authn_context
|
34
35
|
attr_accessor :sp_name_qualifier
|
36
|
+
attr_accessor :idp_name_qualifier
|
35
37
|
attr_accessor :name_identifier_format
|
36
38
|
attr_accessor :name_identifier_value
|
37
39
|
attr_accessor :name_identifier_value_requested
|
38
40
|
attr_accessor :sessionindex
|
39
|
-
attr_accessor :assertion_consumer_logout_service_url
|
40
41
|
attr_accessor :compress_request
|
41
42
|
attr_accessor :compress_response
|
42
43
|
attr_accessor :double_quote_xml_attribute_values
|
@@ -117,21 +118,38 @@ module OneLogin
|
|
117
118
|
@single_logout_service_binding = url
|
118
119
|
end
|
119
120
|
|
120
|
-
#
|
121
|
+
# Calculates the fingerprint of the IdP x509 certificate.
|
122
|
+
# @return [String] The fingerprint
|
121
123
|
#
|
122
|
-
def
|
123
|
-
|
124
|
+
def get_fingerprint
|
125
|
+
idp_cert_fingerprint || begin
|
126
|
+
idp_cert = get_idp_cert
|
127
|
+
if idp_cert
|
128
|
+
Digest::SHA1.hexdigest(idp_cert.to_der).upcase.scan(/../).join(":")
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
124
132
|
|
125
|
-
|
126
|
-
|
133
|
+
# @return [OpenSSL::X509::Certificate|nil] Build the IdP certificate from the settings (previously format it)
|
134
|
+
#
|
135
|
+
def get_idp_cert
|
136
|
+
return nil if idp_cert.nil?
|
137
|
+
|
138
|
+
if idp_cert.respond_to?(:to_pem)
|
139
|
+
idp_cert
|
140
|
+
else
|
141
|
+
return nil if idp_cert.empty?
|
142
|
+
formatted_cert = OneLogin::RubySaml::Utils.format_cert(idp_cert)
|
143
|
+
OpenSSL::X509::Certificate.new(formatted_cert)
|
144
|
+
end
|
127
145
|
end
|
128
146
|
|
129
|
-
# @return [OpenSSL::X509::Certificate|nil] Build the
|
147
|
+
# @return [OpenSSL::X509::Certificate|nil] Build the SP certificate from the settings (previously format it)
|
130
148
|
#
|
131
|
-
def
|
132
|
-
return nil if
|
149
|
+
def get_sp_cert
|
150
|
+
return nil if certificate.nil? || certificate.empty?
|
133
151
|
|
134
|
-
formatted_cert = OneLogin::RubySaml::Utils.format_cert(
|
152
|
+
formatted_cert = OneLogin::RubySaml::Utils.format_cert(certificate)
|
135
153
|
OpenSSL::X509::Certificate.new(formatted_cert)
|
136
154
|
end
|
137
155
|
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require "xml_security"
|
2
|
+
require "time"
|
3
|
+
|
4
|
+
# Only supports SAML 2.0
|
5
|
+
# SAML2 Logout Request (SLO IdP initiated, Parser)
|
6
|
+
module OneLogin
|
7
|
+
module RubySaml
|
8
|
+
class SloLogoutrequest
|
9
|
+
|
10
|
+
ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
|
11
|
+
PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
|
12
|
+
|
13
|
+
# OneLogin::RubySaml::Settings Toolkit settings
|
14
|
+
attr_accessor :settings
|
15
|
+
|
16
|
+
attr_reader :document
|
17
|
+
attr_reader :request
|
18
|
+
attr_reader :options
|
19
|
+
|
20
|
+
def initialize(request, settings = nil, options = {})
|
21
|
+
raise ArgumentError.new("Request cannot be nil") if request.nil?
|
22
|
+
self.settings = settings
|
23
|
+
|
24
|
+
@options = options
|
25
|
+
@request = OneLogin::RubySaml::Utils.decode_raw_saml(request)
|
26
|
+
@document = XMLSecurity::SignedDocument.new(@request)
|
27
|
+
end
|
28
|
+
|
29
|
+
def validate!
|
30
|
+
validate(false)
|
31
|
+
end
|
32
|
+
|
33
|
+
def validate(soft = true)
|
34
|
+
return false unless validate_structure(soft)
|
35
|
+
|
36
|
+
valid_issuer?(soft)
|
37
|
+
end
|
38
|
+
|
39
|
+
def name_id
|
40
|
+
@name_id ||= begin
|
41
|
+
node = REXML::XPath.first(document, "/p:LogoutRequest/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION })
|
42
|
+
Utils.element_text(node)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
alias_method :nameid, :name_id
|
47
|
+
|
48
|
+
def name_id_format
|
49
|
+
@name_id_node ||= REXML::XPath.first(document, "/p:LogoutRequest/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION })
|
50
|
+
@name_id_format ||=
|
51
|
+
if @name_id_node && @name_id_node.attribute("Format")
|
52
|
+
@name_id_node.attribute("Format").value
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
alias_method :nameid_format, :name_id_format
|
57
|
+
|
58
|
+
def id
|
59
|
+
@id ||= begin
|
60
|
+
node = REXML::XPath.first(document, "/p:LogoutRequest", { "p" => PROTOCOL } )
|
61
|
+
node.nil? ? nil : node.attributes['ID']
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def issuer
|
66
|
+
@issuer ||= begin
|
67
|
+
node = REXML::XPath.first(document, "/p:LogoutRequest/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
|
68
|
+
Utils.element_text(node)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def validate_structure(soft = true)
|
75
|
+
Dir.chdir(File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'schemas'))) do
|
76
|
+
@schema = Nokogiri::XML::Schema(IO.read('saml20protocol_schema.xsd'))
|
77
|
+
@xml = Nokogiri::XML(self.document.to_s)
|
78
|
+
end
|
79
|
+
if soft
|
80
|
+
@schema.validate(@xml).map{ return false }
|
81
|
+
else
|
82
|
+
@schema.validate(@xml).map{ |error| validation_error("#{error.message}\n\n#{@xml.to_s}") }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def valid_issuer?(soft = true)
|
87
|
+
return true if settings.nil? || settings.idp_entity_id.nil? || issuer.nil?
|
88
|
+
|
89
|
+
unless OneLogin::RubySaml::Utils.uri_match?(issuer, settings.idp_entity_id)
|
90
|
+
return soft ? false : validation_error("Doesn't match the issuer, expected: <#{self.settings.idp_entity_id}>, but was: <#{issuer}>")
|
91
|
+
end
|
92
|
+
true
|
93
|
+
end
|
94
|
+
|
95
|
+
def validation_error(message)
|
96
|
+
raise ValidationError.new(message)
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|