ruby-saml 0.8.18 → 0.9
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/.gitignore +1 -0
- data/.travis.yml +1 -6
- data/Gemfile +2 -12
- data/README.md +363 -35
- data/Rakefile +14 -0
- data/changelog.md +22 -9
- data/lib/onelogin/ruby-saml/attribute_service.rb +34 -0
- data/lib/onelogin/ruby-saml/attributes.rb +26 -64
- data/lib/onelogin/ruby-saml/authrequest.rb +47 -93
- data/lib/onelogin/ruby-saml/idp_metadata_parser.rb +87 -0
- data/lib/onelogin/ruby-saml/logoutrequest.rb +36 -100
- data/lib/onelogin/ruby-saml/logoutresponse.rb +25 -35
- data/lib/onelogin/ruby-saml/metadata.rb +46 -16
- data/lib/onelogin/ruby-saml/response.rb +63 -373
- data/lib/onelogin/ruby-saml/saml_message.rb +78 -0
- data/lib/onelogin/ruby-saml/settings.rb +54 -122
- data/lib/onelogin/ruby-saml/slo_logoutrequest.rb +25 -71
- data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +37 -102
- data/lib/onelogin/ruby-saml/utils.rb +32 -199
- data/lib/onelogin/ruby-saml/version.rb +1 -1
- data/lib/ruby-saml.rb +5 -2
- data/lib/schemas/{saml20assertion_schema.xsd → saml-schema-assertion-2.0.xsd} +283 -283
- data/lib/schemas/saml-schema-authn-context-2.0.xsd +23 -0
- data/lib/schemas/saml-schema-authn-context-types-2.0.xsd +821 -0
- data/lib/schemas/saml-schema-metadata-2.0.xsd +339 -0
- data/lib/schemas/{saml20protocol_schema.xsd → saml-schema-protocol-2.0.xsd} +302 -302
- data/lib/schemas/sstc-metadata-attr.xsd +35 -0
- data/lib/schemas/sstc-saml-attribute-ext.xsd +25 -0
- data/lib/schemas/sstc-saml-metadata-algsupport-v1.0.xsd +41 -0
- data/lib/schemas/sstc-saml-metadata-ui-v1.0.xsd +89 -0
- data/lib/schemas/{xenc_schema.xsd → xenc-schema.xsd} +1 -11
- data/lib/schemas/xml.xsd +287 -0
- data/lib/schemas/{xmldsig_schema.xsd → xmldsig-core-schema.xsd} +0 -9
- data/lib/xml_security.rb +83 -235
- data/ruby-saml.gemspec +1 -0
- data/test/idp_metadata_parser_test.rb +54 -0
- data/test/logoutrequest_test.rb +68 -155
- data/test/logoutresponse_test.rb +43 -32
- data/test/metadata_test.rb +87 -0
- data/test/request_test.rb +102 -99
- data/test/response_test.rb +181 -495
- data/test/responses/idp_descriptor.xml +3 -0
- data/test/responses/logoutresponse_fixtures.rb +7 -8
- data/test/responses/response_no_cert_and_encrypted_attrs.xml +29 -0
- data/test/responses/response_with_multiple_attribute_values.xml +1 -1
- data/test/responses/slo_request.xml +4 -0
- data/test/settings_test.rb +25 -112
- data/test/slo_logoutrequest_test.rb +40 -50
- data/test/slo_logoutresponse_test.rb +86 -185
- data/test/test_helper.rb +27 -102
- data/test/xml_security_test.rb +114 -337
- metadata +30 -81
- data/lib/onelogin/ruby-saml/setting_error.rb +0 -6
- data/test/certificates/certificate.der +0 -0
- data/test/certificates/formatted_certificate +0 -14
- data/test/certificates/formatted_chained_certificate +0 -42
- data/test/certificates/formatted_private_key +0 -12
- data/test/certificates/formatted_rsa_private_key +0 -12
- data/test/certificates/invalid_certificate1 +0 -1
- data/test/certificates/invalid_certificate2 +0 -1
- data/test/certificates/invalid_certificate3 +0 -12
- data/test/certificates/invalid_chained_certificate1 +0 -1
- data/test/certificates/invalid_private_key1 +0 -1
- data/test/certificates/invalid_private_key2 +0 -1
- data/test/certificates/invalid_private_key3 +0 -10
- data/test/certificates/invalid_rsa_private_key1 +0 -1
- data/test/certificates/invalid_rsa_private_key2 +0 -1
- data/test/certificates/invalid_rsa_private_key3 +0 -10
- data/test/certificates/ruby-saml-2.crt +0 -15
- data/test/requests/logoutrequest_fixtures.rb +0 -47
- data/test/responses/encrypted_new_attack.xml.base64 +0 -1
- data/test/responses/invalids/invalid_issuer_assertion.xml.base64 +0 -1
- data/test/responses/invalids/invalid_issuer_message.xml.base64 +0 -1
- data/test/responses/invalids/multiple_signed.xml.base64 +0 -1
- data/test/responses/invalids/no_signature.xml.base64 +0 -1
- data/test/responses/invalids/response_with_concealed_signed_assertion.xml +0 -51
- data/test/responses/invalids/response_with_doubled_signed_assertion.xml +0 -49
- data/test/responses/invalids/signature_wrapping_attack.xml.base64 +0 -1
- data/test/responses/response_node_text_attack.xml.base64 +0 -1
- data/test/responses/response_with_concealed_signed_assertion.xml +0 -51
- data/test/responses/response_with_doubled_signed_assertion.xml +0 -49
- data/test/responses/response_with_multiple_attribute_statements.xml +0 -72
- data/test/responses/response_with_signed_assertion_3.xml +0 -30
- data/test/responses/response_with_signed_message_and_assertion.xml +0 -34
- data/test/responses/response_with_undefined_recipient.xml.base64 +0 -1
- data/test/responses/response_wrapped.xml.base64 +0 -150
- data/test/responses/valid_response.xml.base64 +0 -1
- data/test/responses/valid_response_without_x509certificate.xml.base64 +0 -1
- data/test/utils_test.rb +0 -231
|
@@ -1,48 +1,30 @@
|
|
|
1
1
|
require "xml_security"
|
|
2
2
|
require "time"
|
|
3
3
|
require "nokogiri"
|
|
4
|
-
require "onelogin/ruby-saml/utils"
|
|
5
|
-
require 'onelogin/ruby-saml/attributes'
|
|
6
4
|
|
|
7
5
|
# Only supports SAML 2.0
|
|
8
6
|
module OneLogin
|
|
9
7
|
module RubySaml
|
|
10
8
|
|
|
11
|
-
class Response
|
|
9
|
+
class Response < SamlMessage
|
|
12
10
|
ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
|
|
13
11
|
PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
|
|
14
12
|
DSIG = "http://www.w3.org/2000/09/xmldsig#"
|
|
15
13
|
|
|
16
14
|
# TODO: This should probably be ctor initialized too... WDYT?
|
|
17
15
|
attr_accessor :settings
|
|
16
|
+
attr_accessor :errors
|
|
18
17
|
|
|
19
18
|
attr_reader :options
|
|
20
19
|
attr_reader :response
|
|
21
20
|
attr_reader :document
|
|
22
21
|
|
|
23
22
|
def initialize(response, options = {})
|
|
23
|
+
@errors = []
|
|
24
24
|
raise ArgumentError.new("Response cannot be nil") if response.nil?
|
|
25
25
|
@options = options
|
|
26
|
-
@response =
|
|
27
|
-
@document = XMLSecurity::SignedDocument.new(@response)
|
|
28
|
-
end
|
|
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
|
|
26
|
+
@response = decode_raw_saml(response)
|
|
27
|
+
@document = XMLSecurity::SignedDocument.new(@response, @errors)
|
|
46
28
|
end
|
|
47
29
|
|
|
48
30
|
def is_valid?
|
|
@@ -53,46 +35,16 @@ module OneLogin
|
|
|
53
35
|
validate(false)
|
|
54
36
|
end
|
|
55
37
|
|
|
56
|
-
def
|
|
57
|
-
@
|
|
58
|
-
xpath_first_from_signed_assertion('/a:Subject/a:NameID')
|
|
59
|
-
end
|
|
38
|
+
def errors
|
|
39
|
+
@errors
|
|
60
40
|
end
|
|
61
41
|
|
|
62
42
|
# The value of the user identifier as designated by the initialization request response
|
|
63
43
|
def name_id
|
|
64
|
-
@name_id ||=
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
44
|
+
@name_id ||= begin
|
|
45
|
+
node = xpath_first_from_signed_assertion('/a:Subject/a:NameID')
|
|
46
|
+
node.nil? ? nil : node.text
|
|
47
|
+
end
|
|
96
48
|
end
|
|
97
49
|
|
|
98
50
|
def sessionindex
|
|
@@ -102,9 +54,9 @@ module OneLogin
|
|
|
102
54
|
end
|
|
103
55
|
end
|
|
104
56
|
|
|
105
|
-
#
|
|
106
|
-
#
|
|
57
|
+
# Returns OneLogin::RubySaml::Attributes enumerable collection.
|
|
107
58
|
# All attributes can be iterated over +attributes.each+ or returned as array by +attributes.all+
|
|
59
|
+
#
|
|
108
60
|
# For backwards compatibility ruby-saml returns by default only the first value for a given attribute with
|
|
109
61
|
# attributes['name']
|
|
110
62
|
# To get all of the attributes, use:
|
|
@@ -113,36 +65,24 @@ module OneLogin
|
|
|
113
65
|
# OneLogin::RubySaml::Attributes.single_value_compatibility = false
|
|
114
66
|
# Now this will return an array:
|
|
115
67
|
# attributes['name']
|
|
116
|
-
#
|
|
117
|
-
# @return [Attributes] OneLogin::RubySaml::Attributes enumerable collection.
|
|
118
|
-
#
|
|
119
68
|
def attributes
|
|
120
69
|
@attr_statements ||= begin
|
|
121
70
|
attributes = Attributes.new
|
|
122
71
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
# NameQualifier, if present is prefixed with a "/" to the value
|
|
136
|
-
else
|
|
137
|
-
REXML::XPath.match(e,'a:NameID', { "a" => ASSERTION }).collect{|n|
|
|
138
|
-
(n.attributes['NameQualifier'] ? n.attributes['NameQualifier'] +"/" : '') + Utils.element_text(n)
|
|
139
|
-
}
|
|
140
|
-
end
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
attributes.add(name, values.flatten)
|
|
144
|
-
end
|
|
72
|
+
stmt_element = xpath_first_from_signed_assertion('/a:AttributeStatement')
|
|
73
|
+
return attributes if stmt_element.nil?
|
|
74
|
+
|
|
75
|
+
stmt_element.elements.each do |attr_element|
|
|
76
|
+
name = attr_element.attributes["Name"]
|
|
77
|
+
values = attr_element.elements.collect{|e|
|
|
78
|
+
# SAMLCore requires that nil AttributeValues MUST contain xsi:nil XML attribute set to "true" or "1"
|
|
79
|
+
# otherwise the value is to be regarded as empty.
|
|
80
|
+
["true", "1"].include?(e.attributes['xsi:nil']) ? nil : e.text.to_s
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
attributes.add(name, values)
|
|
145
84
|
end
|
|
85
|
+
|
|
146
86
|
attributes
|
|
147
87
|
end
|
|
148
88
|
end
|
|
@@ -163,6 +103,13 @@ module OneLogin
|
|
|
163
103
|
end
|
|
164
104
|
end
|
|
165
105
|
|
|
106
|
+
def status_message
|
|
107
|
+
@status_message ||= begin
|
|
108
|
+
node = REXML::XPath.first(document, "/p:Response/p:Status/p:StatusMessage", { "p" => PROTOCOL, "a" => ASSERTION })
|
|
109
|
+
node.text if node
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
166
113
|
# Conditions (if any) for the assertion to run
|
|
167
114
|
def conditions
|
|
168
115
|
@conditions ||= xpath_first_from_signed_assertion('/a:Conditions')
|
|
@@ -180,223 +127,43 @@ module OneLogin
|
|
|
180
127
|
@issuer ||= begin
|
|
181
128
|
node = REXML::XPath.first(document, "/p:Response/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
|
|
182
129
|
node ||= xpath_first_from_signed_assertion('/a:Issuer')
|
|
183
|
-
|
|
184
|
-
end
|
|
185
|
-
end
|
|
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
|
-
|
|
215
|
-
# @return [Array] The Audience elements from the Contitions of the SAML Response.
|
|
216
|
-
#
|
|
217
|
-
def audiences
|
|
218
|
-
@audiences ||= begin
|
|
219
|
-
nodes = xpath_from_signed_assertion('/a:Conditions/a:AudienceRestriction/a:Audience')
|
|
220
|
-
nodes.map { |node| Utils.element_text(node) }.reject(&:empty?)
|
|
130
|
+
node.nil? ? nil : node.text
|
|
221
131
|
end
|
|
222
132
|
end
|
|
223
133
|
|
|
224
134
|
private
|
|
225
135
|
|
|
226
|
-
def validation_error(message)
|
|
227
|
-
raise ValidationError.new(message)
|
|
228
|
-
end
|
|
229
|
-
|
|
230
136
|
def validate(soft = true)
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
validate_audience(soft) &&
|
|
238
|
-
validate_issuer(soft) &&
|
|
239
|
-
validate_signature(soft) &&
|
|
240
|
-
success?
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
# Validates that the SAML Response only contains a single Assertion (encrypted or not).
|
|
244
|
-
# @return [Boolean] True if the SAML Response contains one unique Assertion, otherwise False
|
|
245
|
-
#
|
|
246
|
-
def validate_num_assertion(soft = true)
|
|
247
|
-
assertions = REXML::XPath.match(
|
|
248
|
-
document,
|
|
249
|
-
"//a:Assertion",
|
|
250
|
-
{ "a" => ASSERTION }
|
|
251
|
-
)
|
|
252
|
-
encrypted_assertions = REXML::XPath.match(
|
|
253
|
-
document,
|
|
254
|
-
"//a:EncryptedAssertion",
|
|
255
|
-
{ "a" => ASSERTION }
|
|
256
|
-
)
|
|
257
|
-
|
|
258
|
-
unless assertions.size + encrypted_assertions.size == 1
|
|
259
|
-
return soft ? false : validation_error("SAML Response must contain 1 assertion")
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
true
|
|
263
|
-
end
|
|
264
|
-
|
|
265
|
-
# Validates the Signed elements
|
|
266
|
-
# @return [Boolean] True if there is 1 or 2 Elements signed in the SAML Response
|
|
267
|
-
# an are a Response or an Assertion Element, otherwise False if soft=True
|
|
268
|
-
#
|
|
269
|
-
def validate_signed_elements(soft)
|
|
270
|
-
signature_nodes = REXML::XPath.match(
|
|
271
|
-
document,
|
|
272
|
-
"//ds:Signature",
|
|
273
|
-
{"ds"=>DSIG}
|
|
274
|
-
)
|
|
275
|
-
signed_elements = []
|
|
276
|
-
verified_seis = []
|
|
277
|
-
verified_ids = []
|
|
278
|
-
signature_nodes.each do |signature_node|
|
|
279
|
-
signed_element = signature_node.parent.name
|
|
280
|
-
if signed_element != 'Response' && signed_element != 'Assertion'
|
|
281
|
-
return soft ? false : validation_error("Invalid Signature Element '#{signed_element}'. SAML Response rejected")
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
if signature_node.parent.attributes['ID'].nil?
|
|
285
|
-
return soft ? false : validation_error("Signed Element must contain an ID. SAML Response rejected")
|
|
286
|
-
end
|
|
287
|
-
|
|
288
|
-
id = signature_node.parent.attributes.get_attribute("ID").value
|
|
289
|
-
if verified_ids.include?(id)
|
|
290
|
-
return soft ? false : validation_error("Duplicated ID. SAML Response rejected")
|
|
291
|
-
end
|
|
292
|
-
verified_ids.push(id)
|
|
293
|
-
|
|
294
|
-
# Check that reference URI matches the parent ID and no duplicate References or IDs
|
|
295
|
-
ref = REXML::XPath.first(signature_node, ".//ds:Reference", {"ds"=>DSIG})
|
|
296
|
-
if ref
|
|
297
|
-
uri = ref.attributes.get_attribute("URI")
|
|
298
|
-
if uri && !uri.value.empty?
|
|
299
|
-
sei = uri.value[1..-1]
|
|
300
|
-
|
|
301
|
-
unless sei == id
|
|
302
|
-
return soft ? false : validation_error("Found an invalid Signed Element. SAML Response rejected")
|
|
303
|
-
end
|
|
304
|
-
|
|
305
|
-
if verified_seis.include?(sei)
|
|
306
|
-
return soft ? false : validation_error("Duplicated Reference URI. SAML Response rejected")
|
|
307
|
-
end
|
|
308
|
-
|
|
309
|
-
verified_seis.push(sei)
|
|
310
|
-
end
|
|
311
|
-
end
|
|
312
|
-
|
|
313
|
-
signed_elements << signed_element
|
|
314
|
-
end
|
|
315
|
-
|
|
316
|
-
unless signature_nodes.length < 3 && !signed_elements.empty?
|
|
317
|
-
return soft ? false : validation_error("Found an unexpected number of Signature Element. SAML Response rejected")
|
|
318
|
-
end
|
|
319
|
-
|
|
320
|
-
true
|
|
137
|
+
valid_saml?(document, soft) &&
|
|
138
|
+
validate_response_state(soft) &&
|
|
139
|
+
validate_conditions(soft) &&
|
|
140
|
+
validate_issuer(soft) &&
|
|
141
|
+
document.validate_document(get_fingerprint, soft) &&
|
|
142
|
+
validate_success_status(soft)
|
|
321
143
|
end
|
|
322
144
|
|
|
323
|
-
# Validates the Status of the SAML Response
|
|
324
|
-
# @return [Boolean] True if the SAML Response contains a Success code, otherwise False if soft == false
|
|
325
|
-
# @raise [ValidationError] if soft == false and validation fails
|
|
326
|
-
#
|
|
327
145
|
def validate_success_status(soft = true)
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
error_msg = 'The status code of the Response was not Success'
|
|
333
|
-
status_error_msg = OneLogin::RubySaml::Utils.status_error_msg(error_msg, status_code, status_message)
|
|
334
|
-
return validation_error(status_error_msg)
|
|
335
|
-
end
|
|
336
|
-
|
|
337
|
-
# Checks if the Status has the "Success" code
|
|
338
|
-
# @return [Boolean] True if the StatusCode is Sucess
|
|
339
|
-
#
|
|
340
|
-
def success?
|
|
341
|
-
status_code == "urn:oasis:names:tc:SAML:2.0:status:Success"
|
|
342
|
-
end
|
|
343
|
-
|
|
344
|
-
# @return [String] StatusCode value from a SAML Response.
|
|
345
|
-
#
|
|
346
|
-
def status_code
|
|
347
|
-
@status_code ||= begin
|
|
348
|
-
nodes = REXML::XPath.match(
|
|
349
|
-
document,
|
|
350
|
-
"/p:Response/p:Status/p:StatusCode",
|
|
351
|
-
{ "p" => PROTOCOL }
|
|
352
|
-
)
|
|
353
|
-
if nodes.size == 1
|
|
354
|
-
node = nodes[0]
|
|
355
|
-
code = node.attributes["Value"] if node && node.attributes
|
|
356
|
-
|
|
357
|
-
unless code == "urn:oasis:names:tc:SAML:2.0:status:Success"
|
|
358
|
-
nodes = REXML::XPath.match(
|
|
359
|
-
document,
|
|
360
|
-
"/p:Response/p:Status/p:StatusCode/p:StatusCode",
|
|
361
|
-
{ "p" => PROTOCOL }
|
|
362
|
-
)
|
|
363
|
-
statuses = nodes.collect do |inner_node|
|
|
364
|
-
inner_node.attributes["Value"]
|
|
365
|
-
end
|
|
366
|
-
extra_code = statuses.join(" | ")
|
|
367
|
-
if extra_code
|
|
368
|
-
code = "#{code} | #{extra_code}"
|
|
369
|
-
end
|
|
370
|
-
end
|
|
371
|
-
code
|
|
372
|
-
end
|
|
373
|
-
end
|
|
374
|
-
end
|
|
375
|
-
|
|
376
|
-
# @return [String] the StatusMessage value from a SAML Response.
|
|
377
|
-
#
|
|
378
|
-
def status_message
|
|
379
|
-
@status_message ||= begin
|
|
380
|
-
nodes = REXML::XPath.match(
|
|
381
|
-
document,
|
|
382
|
-
"/p:Response/p:Status/p:StatusMessage",
|
|
383
|
-
{ "p" => PROTOCOL }
|
|
384
|
-
)
|
|
385
|
-
if nodes.size == 1
|
|
386
|
-
Utils.element_text(nodes.first)
|
|
387
|
-
end
|
|
146
|
+
if success?
|
|
147
|
+
true
|
|
148
|
+
else
|
|
149
|
+
soft ? false : validation_error(status_message)
|
|
388
150
|
end
|
|
389
151
|
end
|
|
390
152
|
|
|
391
153
|
def validate_structure(soft = true)
|
|
392
154
|
Dir.chdir(File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'schemas'))) do
|
|
393
|
-
@schema = Nokogiri::XML::Schema(IO.read('
|
|
155
|
+
@schema = Nokogiri::XML::Schema(IO.read('saml-schema-protocol-2.0.xsd'))
|
|
394
156
|
@xml = Nokogiri::XML(self.document.to_s)
|
|
395
157
|
end
|
|
396
158
|
if soft
|
|
397
|
-
@schema.validate(@xml).map{
|
|
159
|
+
@schema.validate(@xml).map{
|
|
160
|
+
@errors << "Schema validation failed";
|
|
161
|
+
return false
|
|
162
|
+
}
|
|
398
163
|
else
|
|
399
|
-
@schema.validate(@xml).map{ |error|
|
|
164
|
+
@schema.validate(@xml).map{ |error| @errors << "#{error.message}\n\n#{@xml.to_s}";
|
|
165
|
+
validation_error("#{error.message}\n\n#{@xml.to_s}")
|
|
166
|
+
}
|
|
400
167
|
end
|
|
401
168
|
end
|
|
402
169
|
|
|
@@ -432,24 +199,13 @@ module OneLogin
|
|
|
432
199
|
node
|
|
433
200
|
end
|
|
434
201
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
document,
|
|
443
|
-
"/p:Response/a:Assertion[@ID=$id]#{subelt}",
|
|
444
|
-
{ "p" => PROTOCOL, "a" => ASSERTION },
|
|
445
|
-
{ 'id' => document.signed_element_id }
|
|
446
|
-
)
|
|
447
|
-
node.concat( REXML::XPath.match(
|
|
448
|
-
document,
|
|
449
|
-
"/p:Response[@ID=$id]/a:Assertion#{subelt}",
|
|
450
|
-
{ "p" => PROTOCOL, "a" => ASSERTION },
|
|
451
|
-
{ 'id' => document.signed_element_id }
|
|
452
|
-
))
|
|
202
|
+
def get_fingerprint
|
|
203
|
+
if settings.idp_cert
|
|
204
|
+
cert = OpenSSL::X509::Certificate.new(settings.idp_cert)
|
|
205
|
+
Digest::SHA1.hexdigest(cert.to_der).upcase.scan(/../).join(":")
|
|
206
|
+
else
|
|
207
|
+
settings.idp_cert_fingerprint
|
|
208
|
+
end
|
|
453
209
|
end
|
|
454
210
|
|
|
455
211
|
def validate_conditions(soft = true)
|
|
@@ -459,10 +215,12 @@ module OneLogin
|
|
|
459
215
|
now = Time.now.utc
|
|
460
216
|
|
|
461
217
|
if not_before && (now + (options[:allowed_clock_drift] || 0)) < not_before
|
|
218
|
+
@errors << "Current time is earlier than NotBefore condition #{(now + (options[:allowed_clock_drift] || 0))} < #{not_before})"
|
|
462
219
|
return soft ? false : validation_error("Current time is earlier than NotBefore condition")
|
|
463
220
|
end
|
|
464
221
|
|
|
465
222
|
if not_on_or_after && now >= not_on_or_after
|
|
223
|
+
@errors << "Current time is on or after NotOnOrAfter condition (#{now} >= #{not_on_or_after})"
|
|
466
224
|
return soft ? false : validation_error("Current time is on or after NotOnOrAfter condition")
|
|
467
225
|
end
|
|
468
226
|
|
|
@@ -472,64 +230,9 @@ module OneLogin
|
|
|
472
230
|
def validate_issuer(soft = true)
|
|
473
231
|
return true if settings.idp_entity_id.nil?
|
|
474
232
|
|
|
475
|
-
|
|
476
|
-
|
|
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
|
-
|
|
491
|
-
def validate_signature(soft = true)
|
|
492
|
-
error_msg = "Invalid Signature on SAML Response"
|
|
493
|
-
|
|
494
|
-
sig_elements = REXML::XPath.match(
|
|
495
|
-
document,
|
|
496
|
-
"/p:Response[@ID=$id]/ds:Signature]",
|
|
497
|
-
{ "p" => PROTOCOL, "ds" => DSIG },
|
|
498
|
-
{ 'id' => document.signed_element_id }
|
|
499
|
-
)
|
|
500
|
-
|
|
501
|
-
# Check signature nodes
|
|
502
|
-
if sig_elements.nil? || sig_elements.size == 0
|
|
503
|
-
sig_elements = REXML::XPath.match(
|
|
504
|
-
document,
|
|
505
|
-
"/p:Response/a:Assertion[@ID=$id]/ds:Signature",
|
|
506
|
-
{"p" => PROTOCOL, "a" => ASSERTION, "ds"=>DSIG},
|
|
507
|
-
{ 'id' => document.signed_element_id }
|
|
508
|
-
)
|
|
509
|
-
end
|
|
510
|
-
|
|
511
|
-
if sig_elements.size != 1
|
|
512
|
-
if sig_elements.size == 0
|
|
513
|
-
error_msg += ". Signed element id ##{doc.signed_element_id} is not found"
|
|
514
|
-
else
|
|
515
|
-
error_msg += ". Signed element id ##{doc.signed_element_id} is found more than once"
|
|
516
|
-
end
|
|
517
|
-
return soft ? false : validation_error(error_msg)
|
|
518
|
-
end
|
|
519
|
-
|
|
520
|
-
opts = {}
|
|
521
|
-
opts[:fingerprint_alg] = OpenSSL::Digest::SHA1.new
|
|
522
|
-
opts[:cert] = settings.get_idp_cert
|
|
523
|
-
fingerprint = settings.get_fingerprint
|
|
524
|
-
|
|
525
|
-
unless fingerprint
|
|
526
|
-
return soft ? false : validation_error("No fingerprint or certificate on settings")
|
|
527
|
-
end
|
|
528
|
-
|
|
529
|
-
unless document.validate_document(fingerprint, soft, opts)
|
|
530
|
-
return soft ? false : validation_error(error_msg)
|
|
233
|
+
unless URI.parse(issuer) == URI.parse(settings.idp_entity_id)
|
|
234
|
+
return soft ? false : validation_error("Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>")
|
|
531
235
|
end
|
|
532
|
-
|
|
533
236
|
true
|
|
534
237
|
end
|
|
535
238
|
|
|
@@ -538,19 +241,6 @@ module OneLogin
|
|
|
538
241
|
Time.parse(node.attributes[attribute])
|
|
539
242
|
end
|
|
540
243
|
end
|
|
541
|
-
|
|
542
|
-
def validate_audience(soft = true)
|
|
543
|
-
return true if audiences.empty? || settings.sp_entity_id.nil? || settings.sp_entity_id.empty?
|
|
544
|
-
|
|
545
|
-
unless audiences.include? settings.sp_entity_id
|
|
546
|
-
s = audiences.count > 1 ? 's' : '';
|
|
547
|
-
error_msg = "Invalid Audience#{s}. The audience#{s} #{audiences.join(',')}, did not match the expected audience #{settings.sp_entity_id}"
|
|
548
|
-
return soft ? false : validation_error(error_msg)
|
|
549
|
-
end
|
|
550
|
-
|
|
551
|
-
true
|
|
552
|
-
end
|
|
553
|
-
|
|
554
244
|
end
|
|
555
245
|
end
|
|
556
246
|
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
require 'cgi'
|
|
2
|
+
require 'zlib'
|
|
3
|
+
require 'base64'
|
|
4
|
+
require "rexml/document"
|
|
5
|
+
require "rexml/xpath"
|
|
6
|
+
|
|
7
|
+
module OneLogin
|
|
8
|
+
module RubySaml
|
|
9
|
+
class SamlMessage
|
|
10
|
+
include REXML
|
|
11
|
+
|
|
12
|
+
ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
|
|
13
|
+
PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
|
|
14
|
+
|
|
15
|
+
def valid_saml?(document, soft = true)
|
|
16
|
+
Dir.chdir(File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'schemas'))) do
|
|
17
|
+
@schema = Nokogiri::XML::Schema(IO.read('saml-schema-protocol-2.0.xsd'))
|
|
18
|
+
@xml = Nokogiri::XML(document.to_s)
|
|
19
|
+
end
|
|
20
|
+
if soft
|
|
21
|
+
@schema.validate(@xml).map{ return false }
|
|
22
|
+
else
|
|
23
|
+
@schema.validate(@xml).map{ |error| validation_error("#{error.message}\n\n#{@xml.to_s}") }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def validation_error(message)
|
|
28
|
+
raise ValidationError.new(message)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def decode_raw_saml(saml)
|
|
34
|
+
if saml =~ /^</
|
|
35
|
+
return saml
|
|
36
|
+
elsif (decoded = decode(saml)) =~ /^</
|
|
37
|
+
return decoded
|
|
38
|
+
elsif (inflated = inflate(decoded)) =~ /^</
|
|
39
|
+
return inflated
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
return nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def encode_raw_saml(saml, settings)
|
|
46
|
+
saml = Zlib::Deflate.deflate(saml, 9)[2..-5] if settings.compress_request
|
|
47
|
+
base64_saml = Base64.encode64(saml)
|
|
48
|
+
return CGI.escape(base64_saml)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def decode(encoded)
|
|
52
|
+
Base64.decode64(encoded)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def encode(encoded)
|
|
56
|
+
Base64.encode64(encoded).gsub(/\n/, "")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def escape(unescaped)
|
|
60
|
+
CGI.escape(unescaped)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def unescape(escaped)
|
|
64
|
+
CGI.unescape(escaped)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def inflate(deflated)
|
|
68
|
+
zlib = Zlib::Inflate.new(-Zlib::MAX_WBITS)
|
|
69
|
+
zlib.inflate(deflated)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def deflate(inflated)
|
|
73
|
+
Zlib::Deflate.deflate(inflated, 9)[2..-5]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|