ruby-saml 0.8.14 → 0.8.18
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of ruby-saml might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/lib/onelogin/ruby-saml/authrequest.rb +5 -1
- data/lib/onelogin/ruby-saml/logoutrequest.rb +7 -2
- data/lib/onelogin/ruby-saml/logoutresponse.rb +19 -27
- data/lib/onelogin/ruby-saml/response.rb +104 -15
- data/lib/onelogin/ruby-saml/settings.rb +28 -10
- data/lib/onelogin/ruby-saml/slo_logoutrequest.rb +112 -0
- data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +21 -13
- data/lib/onelogin/ruby-saml/utils.rb +27 -0
- data/lib/onelogin/ruby-saml/version.rb +1 -1
- data/lib/ruby-saml.rb +1 -0
- data/lib/xml_security.rb +5 -1
- 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/logoutrequest_test.rb +11 -0
- data/test/logoutresponse_test.rb +10 -17
- data/test/request_test.rb +10 -0
- data/test/requests/logoutrequest_fixtures.rb +47 -0
- data/test/response_test.rb +60 -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/logoutresponse_fixtures.rb +7 -6
- data/test/settings_test.rb +106 -0
- data/test/slo_logoutrequest_test.rb +73 -0
- data/test/slo_logoutresponse_test.rb +19 -0
- data/test/utils_test.rb +191 -1
- data/test/xml_security_test.rb +5 -0
- metadata +57 -24
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f7f02b4fb1490e44140c1e24ea61bf0ef061f0da
|
4
|
+
data.tar.gz: 3b2fec942140bb2fd2e49a17fc29dd0d0327d814
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a2dade6d6d672b213a3f8d3af088edfa208f3af4482aae033a40ff8ac9c500ca48fded7eee073e67dd6f37daa27786a6303777e0381a3338f987f87a63f460a2
|
7
|
+
data.tar.gz: 0cc6bb2264de5c688545bcb24794313758a97cca3c9bfa5c69b07331ffbc719acb1a69039ec39aa2e5e6817e8917e7495da7c07a41eff120d57f30e9708962bc
|
@@ -9,7 +9,7 @@ module OneLogin
|
|
9
9
|
|
10
10
|
class Authrequest
|
11
11
|
# AuthNRequest ID
|
12
|
-
|
12
|
+
attr_accessor :uuid
|
13
13
|
|
14
14
|
# Initializes the AuthNRequest. An Authrequest Object.
|
15
15
|
# Asigns an ID, a random uuid.
|
@@ -18,6 +18,10 @@ module OneLogin
|
|
18
18
|
@uuid = OneLogin::RubySaml::Utils.uuid
|
19
19
|
end
|
20
20
|
|
21
|
+
def request_id
|
22
|
+
@uuid
|
23
|
+
end
|
24
|
+
|
21
25
|
def create(settings, params = {})
|
22
26
|
params = create_params(settings, params)
|
23
27
|
params_prefix = (settings.idp_sso_target_url =~ /\?/) ? '&' : '?'
|
@@ -10,12 +10,16 @@ module OneLogin
|
|
10
10
|
|
11
11
|
class Logoutrequest
|
12
12
|
|
13
|
-
|
13
|
+
attr_accessor :uuid
|
14
14
|
|
15
15
|
def initialize
|
16
16
|
@uuid = OneLogin::RubySaml::Utils.uuid
|
17
17
|
end
|
18
18
|
|
19
|
+
def request_id
|
20
|
+
@uuid
|
21
|
+
end
|
22
|
+
|
19
23
|
def create(settings, params={})
|
20
24
|
params = create_params(settings, params)
|
21
25
|
params_prefix = (settings.idp_slo_target_url =~ /\?/) ? '&' : '?'
|
@@ -114,7 +118,8 @@ module OneLogin
|
|
114
118
|
|
115
119
|
if settings.name_identifier_value
|
116
120
|
name_id = root.add_element "saml:NameID", { "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion" }
|
117
|
-
|
121
|
+
nameid.attributes['NameQualifier'] = settings.idp_name_qualifier if settings.idp_name_qualifier
|
122
|
+
nameid.attributes['SPNameQualifier'] = settings.sp_name_qualifier if settings.sp_name_qualifier
|
118
123
|
name_id.attributes['Format'] = settings.name_identifier_format if settings.name_identifier_format
|
119
124
|
name_id.text = settings.name_identifier_value
|
120
125
|
end
|
@@ -29,7 +29,18 @@ module OneLogin
|
|
29
29
|
|
30
30
|
@options = options
|
31
31
|
@response = OneLogin::RubySaml::Utils.decode_raw_saml(response)
|
32
|
-
@document = XMLSecurity::SignedDocument.new(response)
|
32
|
+
@document = XMLSecurity::SignedDocument.new(@response)
|
33
|
+
end
|
34
|
+
|
35
|
+
def response_id
|
36
|
+
@response_id ||= begin
|
37
|
+
node = REXML::XPath.first(
|
38
|
+
document,
|
39
|
+
"/p:LogoutResponse",
|
40
|
+
{ "p" => PROTOCOL }
|
41
|
+
)
|
42
|
+
node.nil? ? nil : node.attributes['ID']
|
43
|
+
end
|
33
44
|
end
|
34
45
|
|
35
46
|
def validate!
|
@@ -37,7 +48,7 @@ module OneLogin
|
|
37
48
|
end
|
38
49
|
|
39
50
|
def validate(soft = true)
|
40
|
-
return false unless
|
51
|
+
return false unless validate_structure(soft)
|
41
52
|
|
42
53
|
valid_in_response_to?(soft) && valid_issuer?(soft) && success?(soft)
|
43
54
|
end
|
@@ -51,7 +62,7 @@ module OneLogin
|
|
51
62
|
|
52
63
|
def in_response_to
|
53
64
|
@in_response_to ||= begin
|
54
|
-
node = REXML::XPath.first(document, "/p:LogoutResponse", { "p" => PROTOCOL
|
65
|
+
node = REXML::XPath.first(document, "/p:LogoutResponse", { "p" => PROTOCOL })
|
55
66
|
node.nil? ? nil : node.attributes['InResponseTo']
|
56
67
|
end
|
57
68
|
end
|
@@ -59,7 +70,6 @@ module OneLogin
|
|
59
70
|
def issuer
|
60
71
|
@issuer ||= begin
|
61
72
|
node = REXML::XPath.first(document, "/p:LogoutResponse/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
|
62
|
-
node ||= REXML::XPath.first(document, "/p:LogoutResponse/a:Assertion/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
|
63
73
|
Utils.element_text(node)
|
64
74
|
end
|
65
75
|
end
|
@@ -73,7 +83,7 @@ module OneLogin
|
|
73
83
|
|
74
84
|
private
|
75
85
|
|
76
|
-
def
|
86
|
+
def validate_structure(soft = true)
|
77
87
|
Dir.chdir(File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'schemas'))) do
|
78
88
|
@schema = Nokogiri::XML::Schema(IO.read('saml20protocol_schema.xsd'))
|
79
89
|
@xml = Nokogiri::XML(self.document.to_s)
|
@@ -85,26 +95,6 @@ module OneLogin
|
|
85
95
|
end
|
86
96
|
end
|
87
97
|
|
88
|
-
def valid_state?(soft = true)
|
89
|
-
if response.empty?
|
90
|
-
return soft ? false : validation_error("Blank response")
|
91
|
-
end
|
92
|
-
|
93
|
-
if settings.nil?
|
94
|
-
return soft ? false : validation_error("No settings on response")
|
95
|
-
end
|
96
|
-
|
97
|
-
if settings.sp_entity_id.nil?
|
98
|
-
return soft ? false : validation_error("No sp_entity_id in settings")
|
99
|
-
end
|
100
|
-
|
101
|
-
if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil?
|
102
|
-
return soft ? false : validation_error("No fingerprint or certificate on settings")
|
103
|
-
end
|
104
|
-
|
105
|
-
true
|
106
|
-
end
|
107
|
-
|
108
98
|
def valid_in_response_to?(soft = true)
|
109
99
|
return true unless self.options.has_key? :matches_request_id
|
110
100
|
|
@@ -116,8 +106,10 @@ module OneLogin
|
|
116
106
|
end
|
117
107
|
|
118
108
|
def valid_issuer?(soft = true)
|
119
|
-
|
120
|
-
|
109
|
+
return true if settings.nil? || settings.idp_entity_id.nil? || issuer.nil?
|
110
|
+
|
111
|
+
unless OneLogin::RubySaml::Utils.uri_match?(issuer, settings.idp_entity_id)
|
112
|
+
return soft ? false : validation_error("Doesn't match the issuer, expected: <#{self.settings.idp_entity_id}>, but was: <#{issuer}>")
|
121
113
|
end
|
122
114
|
true
|
123
115
|
end
|
@@ -27,6 +27,24 @@ module OneLogin
|
|
27
27
|
@document = XMLSecurity::SignedDocument.new(@response)
|
28
28
|
end
|
29
29
|
|
30
|
+
def response_id
|
31
|
+
@response_id ||= begin
|
32
|
+
node = REXML::XPath.first(
|
33
|
+
document,
|
34
|
+
"/p:Response",
|
35
|
+
{ "p" => PROTOCOL }
|
36
|
+
)
|
37
|
+
node.nil? ? nil : node.attributes['ID']
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def assertion_id
|
42
|
+
@assertion_id ||= begin
|
43
|
+
node = xpath_first_from_signed_assertion("")
|
44
|
+
node.nil? ? nil : node.attributes['ID']
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
30
48
|
def is_valid?
|
31
49
|
validate
|
32
50
|
end
|
@@ -35,16 +53,48 @@ module OneLogin
|
|
35
53
|
validate(false)
|
36
54
|
end
|
37
55
|
|
38
|
-
|
39
|
-
def name_id
|
56
|
+
def name_id_node
|
40
57
|
@name_id ||= begin
|
41
|
-
|
42
|
-
Utils.element_text(node)
|
58
|
+
xpath_first_from_signed_assertion('/a:Subject/a:NameID')
|
43
59
|
end
|
44
60
|
end
|
45
61
|
|
62
|
+
# The value of the user identifier as designated by the initialization request response
|
63
|
+
def name_id
|
64
|
+
@name_id ||= Utils.element_text(name_id_node)
|
65
|
+
end
|
66
|
+
|
46
67
|
alias nameid name_id
|
47
68
|
|
69
|
+
# @return [String] the NameID Format provided by the SAML response from the IdP.
|
70
|
+
#
|
71
|
+
def name_id_format
|
72
|
+
@name_id_format ||=
|
73
|
+
if name_id_node && name_id_node.attribute("Format")
|
74
|
+
name_id_node.attribute("Format").value
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
alias_method :nameid_format, :name_id_format
|
79
|
+
|
80
|
+
# @return [String] the NameID SPNameQualifier provided by the SAML response from the IdP.
|
81
|
+
#
|
82
|
+
def name_id_spnamequalifier
|
83
|
+
@name_id_spnamequalifier ||=
|
84
|
+
if name_id_node && name_id_node.attribute("SPNameQualifier")
|
85
|
+
name_id_node.attribute("SPNameQualifier").value
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# @return [String] the NameID NameQualifier provided by the SAML response from the IdP.
|
90
|
+
#
|
91
|
+
def name_id_namequalifier
|
92
|
+
@name_id_namequalifier ||=
|
93
|
+
if name_id_node && name_id_node.attribute("NameQualifier")
|
94
|
+
name_id_node.attribute("NameQualifier").value
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
48
98
|
def sessionindex
|
49
99
|
@sessionindex ||= begin
|
50
100
|
node = xpath_first_from_signed_assertion('/a:AuthnStatement')
|
@@ -134,6 +184,34 @@ module OneLogin
|
|
134
184
|
end
|
135
185
|
end
|
136
186
|
|
187
|
+
# Gets the Issuers (from Response and Assertion).
|
188
|
+
# (returns the first node that matches the supplied xpath from the Response and from the Assertion)
|
189
|
+
# @return [Array] Array with the Issuers (REXML::Element)
|
190
|
+
#
|
191
|
+
def issuers
|
192
|
+
@issuers ||= begin
|
193
|
+
issuer_response_nodes = REXML::XPath.match(
|
194
|
+
document,
|
195
|
+
"/p:Response/a:Issuer",
|
196
|
+
{ "p" => PROTOCOL, "a" => ASSERTION }
|
197
|
+
)
|
198
|
+
|
199
|
+
unless issuer_response_nodes.size == 1
|
200
|
+
error_msg = "Issuer of the Response not found or multiple."
|
201
|
+
raise ValidationError.new(error_msg)
|
202
|
+
end
|
203
|
+
|
204
|
+
issuer_assertion_nodes = xpath_from_signed_assertion("/a:Issuer")
|
205
|
+
unless issuer_assertion_nodes.size == 1
|
206
|
+
error_msg = "Issuer of the Assertion not found or multiple."
|
207
|
+
raise ValidationError.new(error_msg)
|
208
|
+
end
|
209
|
+
|
210
|
+
nodes = issuer_response_nodes + issuer_assertion_nodes
|
211
|
+
nodes.map { |node| Utils.element_text(node) }.compact.uniq
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
137
215
|
# @return [Array] The Audience elements from the Contitions of the SAML Response.
|
138
216
|
#
|
139
217
|
def audiences
|
@@ -157,6 +235,7 @@ module OneLogin
|
|
157
235
|
validate_response_state(soft) &&
|
158
236
|
validate_conditions(soft) &&
|
159
237
|
validate_audience(soft) &&
|
238
|
+
validate_issuer(soft) &&
|
160
239
|
validate_signature(soft) &&
|
161
240
|
success?
|
162
241
|
end
|
@@ -373,15 +452,6 @@ module OneLogin
|
|
373
452
|
))
|
374
453
|
end
|
375
454
|
|
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
455
|
def validate_conditions(soft = true)
|
386
456
|
return true if conditions.nil?
|
387
457
|
return true if options[:skip_conditions]
|
@@ -399,6 +469,25 @@ module OneLogin
|
|
399
469
|
true
|
400
470
|
end
|
401
471
|
|
472
|
+
def validate_issuer(soft = true)
|
473
|
+
return true if settings.idp_entity_id.nil?
|
474
|
+
|
475
|
+
begin
|
476
|
+
obtained_issuers = issuers
|
477
|
+
rescue ValidationError => e
|
478
|
+
return soft ? false : validation_error("Error while extracting issuers")
|
479
|
+
end
|
480
|
+
|
481
|
+
obtained_issuers.each do |issuer|
|
482
|
+
unless OneLogin::RubySaml::Utils.uri_match?(issuer, settings.idp_entity_id)
|
483
|
+
error_msg = "Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>"
|
484
|
+
return soft ? false : validation_error(error_msg)
|
485
|
+
end
|
486
|
+
end
|
487
|
+
|
488
|
+
true
|
489
|
+
end
|
490
|
+
|
402
491
|
def validate_signature(soft = true)
|
403
492
|
error_msg = "Invalid Signature on SAML Response"
|
404
493
|
|
@@ -430,8 +519,8 @@ module OneLogin
|
|
430
519
|
|
431
520
|
opts = {}
|
432
521
|
opts[:fingerprint_alg] = OpenSSL::Digest::SHA1.new
|
433
|
-
opts[:cert] = settings.
|
434
|
-
fingerprint = get_fingerprint
|
522
|
+
opts[:cert] = settings.get_idp_cert
|
523
|
+
fingerprint = settings.get_fingerprint
|
435
524
|
|
436
525
|
unless fingerprint
|
437
526
|
return soft ? false : validation_error("No fingerprint or certificate on settings")
|
@@ -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,112 @@
|
|
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 request_id
|
30
|
+
@request_id ||= begin
|
31
|
+
node = REXML::XPath.first(
|
32
|
+
document,
|
33
|
+
"/p:LogoutRequest",
|
34
|
+
{ "p" => PROTOCOL }
|
35
|
+
)
|
36
|
+
node.nil? ? nil : node.attributes['ID']
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def validate!
|
41
|
+
validate(false)
|
42
|
+
end
|
43
|
+
|
44
|
+
def validate(soft = true)
|
45
|
+
return false unless validate_structure(soft)
|
46
|
+
|
47
|
+
valid_issuer?(soft)
|
48
|
+
end
|
49
|
+
|
50
|
+
def name_id
|
51
|
+
@name_id ||= begin
|
52
|
+
node = REXML::XPath.first(document, "/p:LogoutRequest/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION })
|
53
|
+
Utils.element_text(node)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
alias_method :nameid, :name_id
|
58
|
+
|
59
|
+
def name_id_format
|
60
|
+
@name_id_node ||= REXML::XPath.first(document, "/p:LogoutRequest/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION })
|
61
|
+
@name_id_format ||=
|
62
|
+
if @name_id_node && @name_id_node.attribute("Format")
|
63
|
+
@name_id_node.attribute("Format").value
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
alias_method :nameid_format, :name_id_format
|
68
|
+
|
69
|
+
def id
|
70
|
+
@id ||= begin
|
71
|
+
node = REXML::XPath.first(document, "/p:LogoutRequest", { "p" => PROTOCOL } )
|
72
|
+
node.nil? ? nil : node.attributes['ID']
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def issuer
|
77
|
+
@issuer ||= begin
|
78
|
+
node = REXML::XPath.first(document, "/p:LogoutRequest/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
|
79
|
+
Utils.element_text(node)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def validate_structure(soft = true)
|
86
|
+
Dir.chdir(File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'schemas'))) do
|
87
|
+
@schema = Nokogiri::XML::Schema(IO.read('saml20protocol_schema.xsd'))
|
88
|
+
@xml = Nokogiri::XML(self.document.to_s)
|
89
|
+
end
|
90
|
+
if soft
|
91
|
+
@schema.validate(@xml).map{ return false }
|
92
|
+
else
|
93
|
+
@schema.validate(@xml).map{ |error| validation_error("#{error.message}\n\n#{@xml.to_s}") }
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def valid_issuer?(soft = true)
|
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}>")
|
102
|
+
end
|
103
|
+
true
|
104
|
+
end
|
105
|
+
|
106
|
+
def validation_error(message)
|
107
|
+
raise ValidationError.new(message)
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -12,7 +12,7 @@ module OneLogin
|
|
12
12
|
class SloLogoutresponse
|
13
13
|
|
14
14
|
# Logout Response ID
|
15
|
-
|
15
|
+
attr_accessor :uuid
|
16
16
|
|
17
17
|
# Initializes the Logout Response. A SloLogoutresponse Object.
|
18
18
|
# Asigns an ID, a random uuid.
|
@@ -21,15 +21,20 @@ module OneLogin
|
|
21
21
|
@uuid = OneLogin::RubySaml::Utils.uuid
|
22
22
|
end
|
23
23
|
|
24
|
+
def response_id
|
25
|
+
@uuid
|
26
|
+
end
|
27
|
+
|
24
28
|
# Creates the Logout Response string.
|
25
29
|
# @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
|
26
30
|
# @param request_id [String] The ID of the LogoutRequest sent by this SP to the IdP. That ID will be placed as the InResponseTo in the logout response
|
27
31
|
# @param logout_message [String] The Message to be placed as StatusMessage in the logout response
|
28
32
|
# @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
|
33
|
+
# @param logout_status_code [String] The StatusCode to be placed as StatusMessage in the logout response
|
29
34
|
# @return [String] Logout Request string that includes the SAMLRequest
|
30
35
|
#
|
31
|
-
def create(settings, request_id = nil, logout_message = nil, params = {})
|
32
|
-
params = create_params(settings, request_id, logout_message, params)
|
36
|
+
def create(settings, request_id = nil, logout_message = nil, params = {}, logout_status_code = nil)
|
37
|
+
params = create_params(settings, request_id, logout_message, params, logout_status_code)
|
33
38
|
params_prefix = (settings.idp_slo_target_url =~ /\?/) ? '&' : '?'
|
34
39
|
saml_response = CGI.escape(params.delete("SAMLResponse"))
|
35
40
|
response_params = "#{params_prefix}SAMLResponse=#{saml_response}"
|
@@ -45,9 +50,10 @@ module OneLogin
|
|
45
50
|
# @param request_id [String] The ID of the LogoutRequest sent by this SP to the IdP. That ID will be placed as the InResponseTo in the logout response
|
46
51
|
# @param logout_message [String] The Message to be placed as StatusMessage in the logout response
|
47
52
|
# @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
|
53
|
+
# @param logout_status_code [String] The StatusCode to be placed as StatusMessage in the logout response
|
48
54
|
# @return [Hash] Parameters
|
49
55
|
#
|
50
|
-
def create_params(settings, request_id = nil, logout_message = nil, params = {})
|
56
|
+
def create_params(settings, request_id = nil, logout_message = nil, params = {}, logout_status_code = nil)
|
51
57
|
# The method expects :RelayState but sometimes we get 'RelayState' instead.
|
52
58
|
# Based on the HashWithIndifferentAccess value in Rails we could experience
|
53
59
|
# conflicts so this line will solve them.
|
@@ -58,7 +64,7 @@ module OneLogin
|
|
58
64
|
params.delete('RelayState')
|
59
65
|
end
|
60
66
|
|
61
|
-
response_doc = create_logout_response_xml_doc(settings, request_id, logout_message)
|
67
|
+
response_doc = create_logout_response_xml_doc(settings, request_id, logout_message, logout_status_code)
|
62
68
|
response_doc.context[:attribute_quote] = :quote if settings.double_quote_xml_attribute_values
|
63
69
|
|
64
70
|
response = ""
|
@@ -102,14 +108,15 @@ module OneLogin
|
|
102
108
|
# @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
|
103
109
|
# @param request_id [String] The ID of the LogoutRequest sent by this SP to the IdP. That ID will be placed as the InResponseTo in the logout response
|
104
110
|
# @param logout_message [String] The Message to be placed as StatusMessage in the logout response
|
111
|
+
# @param logout_status_code [String] The StatusCode to be placed as StatusMessage in the logout response
|
105
112
|
# @return [String] The SAMLResponse String.
|
106
113
|
#
|
107
|
-
def create_logout_response_xml_doc(settings, request_id = nil, logout_message = nil)
|
108
|
-
document = create_xml_document(settings, request_id, logout_message)
|
114
|
+
def create_logout_response_xml_doc(settings, request_id = nil, logout_message = nil, logout_status_code = nil)
|
115
|
+
document = create_xml_document(settings, request_id, logout_message, logout_status_code)
|
109
116
|
sign_document(document, settings)
|
110
117
|
end
|
111
118
|
|
112
|
-
def create_xml_document(settings, request_id = nil, logout_message = nil)
|
119
|
+
def create_xml_document(settings, request_id = nil, logout_message = nil, status_code = nil)
|
113
120
|
time = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
|
114
121
|
|
115
122
|
response_doc = XMLSecurity::Document.new
|
@@ -127,14 +134,15 @@ module OneLogin
|
|
127
134
|
issuer.text = settings.sp_entity_id
|
128
135
|
end
|
129
136
|
|
130
|
-
# add
|
137
|
+
# add status
|
131
138
|
status = root.add_element 'samlp:Status'
|
132
139
|
|
133
|
-
#
|
134
|
-
status_code
|
135
|
-
|
140
|
+
# status code
|
141
|
+
status_code ||= 'urn:oasis:names:tc:SAML:2.0:status:Success'
|
142
|
+
status_code_elem = status.add_element 'samlp:StatusCode'
|
143
|
+
status_code_elem.attributes['Value'] = status_code
|
136
144
|
|
137
|
-
#
|
145
|
+
# status message
|
138
146
|
logout_message ||= 'Successfully Signed Out'
|
139
147
|
status_message = status.add_element 'samlp:StatusMessage'
|
140
148
|
status_message.text = logout_message
|
@@ -179,6 +179,33 @@ module OneLogin
|
|
179
179
|
Zlib::Deflate.deflate(inflated, 9)[2..-5]
|
180
180
|
end
|
181
181
|
|
182
|
+
# Given two strings, attempt to match them as URIs using Rails' parse method. If they can be parsed,
|
183
|
+
# then the fully-qualified domain name and the host should performa a case-insensitive match, per the
|
184
|
+
# RFC for URIs. If Rails can not parse the string in to URL pieces, return a boolean match of the
|
185
|
+
# two strings. This maintains the previous functionality.
|
186
|
+
# @return [Boolean]
|
187
|
+
def self.uri_match?(destination_url, settings_url)
|
188
|
+
dest_uri = URI.parse(destination_url)
|
189
|
+
acs_uri = URI.parse(settings_url)
|
190
|
+
|
191
|
+
if dest_uri.scheme.nil? || acs_uri.scheme.nil? || dest_uri.host.nil? || acs_uri.host.nil?
|
192
|
+
raise URI::InvalidURIError
|
193
|
+
else
|
194
|
+
dest_uri.scheme.downcase == acs_uri.scheme.downcase &&
|
195
|
+
dest_uri.host.downcase == acs_uri.host.downcase &&
|
196
|
+
dest_uri.path == acs_uri.path &&
|
197
|
+
dest_uri.query == acs_uri.query
|
198
|
+
end
|
199
|
+
rescue URI::InvalidURIError
|
200
|
+
original_uri_match?(destination_url, settings_url)
|
201
|
+
end
|
202
|
+
|
203
|
+
# If Rails' URI.parse can't match to valid URL, default back to the original matching service.
|
204
|
+
# @return [Boolean]
|
205
|
+
def self.original_uri_match?(destination_url, settings_url)
|
206
|
+
destination_url == settings_url
|
207
|
+
end
|
208
|
+
|
182
209
|
end
|
183
210
|
end
|
184
211
|
end
|
data/lib/ruby-saml.rb
CHANGED
@@ -2,6 +2,7 @@ require 'onelogin/ruby-saml/logging'
|
|
2
2
|
require 'onelogin/ruby-saml/authrequest'
|
3
3
|
require 'onelogin/ruby-saml/logoutrequest'
|
4
4
|
require 'onelogin/ruby-saml/logoutresponse'
|
5
|
+
require 'onelogin/ruby-saml/slo_logoutrequest'
|
5
6
|
require 'onelogin/ruby-saml/slo_logoutresponse'
|
6
7
|
require 'onelogin/ruby-saml/response'
|
7
8
|
require 'onelogin/ruby-saml/settings'
|
data/lib/xml_security.rb
CHANGED
@@ -222,7 +222,11 @@ module XMLSecurity
|
|
222
222
|
end
|
223
223
|
else
|
224
224
|
if options[:cert]
|
225
|
-
|
225
|
+
cert = options[:cert]
|
226
|
+
if cert.is_a? String
|
227
|
+
cert = OpenSSL::X509::Certificate.new(cert)
|
228
|
+
end
|
229
|
+
base64_cert = Base64.encode64(cert.to_pem)
|
226
230
|
else
|
227
231
|
return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Certificate element missing in response (ds:X509Certificate) and not cert provided at settings"))
|
228
232
|
end
|
Binary file
|
@@ -0,0 +1,14 @@
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
2
|
+
MIICPDCCAaWgAwIBAgIIEiC/9HMAWWAwDQYJKoZIhvcNAQEFBQAwTzELMAkGA1UE
|
3
|
+
BhMCVVMxDDAKBgNVBAoTA2libTEMMAoGA1UECxMDc3NvMSQwIgYDVQQDExtjMjVh
|
4
|
+
MDI3Ny50b3JvbnRvLmNhLmlibS5jb20wHhcNMTEwNTI0MTYzNTQ4WhcNMjEwNTIx
|
5
|
+
wsQMPBj4WQTNzTYMCQYDVQQGEwJVUzEMMAoGA1UEChMDaWJtMQwwCgYDVQQLEwNz
|
6
|
+
c28xJDAiBgNVBAMTG2MyNWEwMjc3LnRvcm9udG8uY2EuaWJtLmNvbTCBnzANBgkq
|
7
|
+
hkiG9w0BAQEFAAOBjQAwgYkCgYEAgzfYQZuf5FVdJTcrsIQZ+YHTPjOsw2MGo0jC
|
8
|
+
mdGMcp4brWeFgk1OVaOmytPx6P76wHWR436AleX3crHBPd8gPxuZdnvBQ7PkrKpw
|
9
|
+
Vvaq52juenFrho8JY0TeVgVkY5jAh45YzytjP2y2k/cGQurI/56NT0PpQJ0S1G3N
|
10
|
+
4eTg718CAwEAAaMhMB8wHQYDVR0OBBYEFCYVLJqcJ7WgdzGIsuJ/TzDGDqinMA0G
|
11
|
+
CSqGSIb3DQEBBQUAA4GBAB80bIePf+qWDvWe+9bEEnbFTw7pCknLexxZ0AMqrsmZ
|
12
|
+
+4jmI+evP1JZYCjfIg9X+MBH01hfp5dFcetz3o6w6SkV+BxLYLgfcy5KUcYsIM/1
|
13
|
+
2Zkedj87bS1glzOy5B89pKD2DMbu6828Abzgc+4lyQ2ASifsqM4cZdVayzo8n+dQ
|
14
|
+
-----END CERTIFICATE-----
|