ruby-saml 0.8.12
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 -0
- data/.document +5 -0
- data/.gitignore +12 -0
- data/.travis.yml +11 -0
- data/Gemfile +37 -0
- data/LICENSE +19 -0
- data/README.md +160 -0
- data/Rakefile +27 -0
- data/changelog.md +24 -0
- data/lib/onelogin/ruby-saml/attributes.rb +147 -0
- data/lib/onelogin/ruby-saml/authrequest.rb +168 -0
- data/lib/onelogin/ruby-saml/logging.rb +26 -0
- data/lib/onelogin/ruby-saml/logoutrequest.rb +161 -0
- data/lib/onelogin/ruby-saml/logoutresponse.rb +153 -0
- data/lib/onelogin/ruby-saml/metadata.rb +66 -0
- data/lib/onelogin/ruby-saml/response.rb +426 -0
- data/lib/onelogin/ruby-saml/setting_error.rb +6 -0
- data/lib/onelogin/ruby-saml/settings.rb +166 -0
- data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +158 -0
- data/lib/onelogin/ruby-saml/utils.rb +119 -0
- data/lib/onelogin/ruby-saml/validation_error.rb +7 -0
- data/lib/onelogin/ruby-saml/version.rb +5 -0
- data/lib/ruby-saml.rb +12 -0
- data/lib/schemas/saml20assertion_schema.xsd +283 -0
- data/lib/schemas/saml20protocol_schema.xsd +302 -0
- data/lib/schemas/xenc_schema.xsd +146 -0
- data/lib/schemas/xmldsig_schema.xsd +318 -0
- data/lib/xml_security.rb +292 -0
- data/ruby-saml.gemspec +28 -0
- data/test/certificates/certificate1 +12 -0
- data/test/certificates/r1_certificate2_base64 +1 -0
- data/test/certificates/ruby-saml.crt +14 -0
- data/test/certificates/ruby-saml.key +15 -0
- data/test/logoutrequest_test.rb +244 -0
- data/test/logoutresponse_test.rb +112 -0
- data/test/request_test.rb +229 -0
- data/test/response_test.rb +475 -0
- data/test/responses/adfs_response_sha1.xml +46 -0
- data/test/responses/adfs_response_sha256.xml +46 -0
- data/test/responses/adfs_response_sha384.xml +46 -0
- data/test/responses/adfs_response_sha512.xml +46 -0
- data/test/responses/encrypted_new_attack.xml.base64 +1 -0
- data/test/responses/logoutresponse_fixtures.rb +67 -0
- data/test/responses/no_signature_ns.xml +48 -0
- data/test/responses/open_saml_response.xml +56 -0
- data/test/responses/r1_response6.xml.base64 +1 -0
- data/test/responses/response1.xml.base64 +1 -0
- data/test/responses/response2.xml.base64 +79 -0
- data/test/responses/response3.xml.base64 +66 -0
- data/test/responses/response4.xml.base64 +93 -0
- data/test/responses/response5.xml.base64 +102 -0
- data/test/responses/response_eval.xml +7 -0
- data/test/responses/response_node_text_attack.xml.base64 +1 -0
- data/test/responses/response_with_ampersands.xml +139 -0
- data/test/responses/response_with_ampersands.xml.base64 +93 -0
- data/test/responses/response_with_concealed_signed_assertion.xml +51 -0
- data/test/responses/response_with_doubled_signed_assertion.xml +49 -0
- data/test/responses/response_with_multiple_attribute_statements.xml +72 -0
- data/test/responses/response_with_multiple_attribute_values.xml +67 -0
- data/test/responses/response_wrapped.xml.base64 +150 -0
- data/test/responses/simple_saml_php.xml +71 -0
- data/test/responses/starfield_response.xml.base64 +1 -0
- data/test/responses/valid_response.xml.base64 +1 -0
- data/test/responses/wrapped_response_2.xml.base64 +150 -0
- data/test/settings_test.rb +47 -0
- data/test/slo_logoutresponse_test.rb +226 -0
- data/test/test_helper.rb +155 -0
- data/test/utils_test.rb +41 -0
- data/test/xml_security_test.rb +158 -0
- metadata +178 -0
@@ -0,0 +1,66 @@
|
|
1
|
+
require "rexml/document"
|
2
|
+
require "rexml/xpath"
|
3
|
+
require "uri"
|
4
|
+
|
5
|
+
# Class to return SP metadata based on the settings requested.
|
6
|
+
# Return this XML in a controller, then give that URL to the the
|
7
|
+
# IdP administrator. The IdP will poll the URL and your settings
|
8
|
+
# will be updated automatically
|
9
|
+
module OneLogin
|
10
|
+
module RubySaml
|
11
|
+
include REXML
|
12
|
+
class Metadata
|
13
|
+
def generate(settings)
|
14
|
+
meta_doc = REXML::Document.new
|
15
|
+
root = meta_doc.add_element "md:EntityDescriptor", {
|
16
|
+
"xmlns:md" => "urn:oasis:names:tc:SAML:2.0:metadata"
|
17
|
+
}
|
18
|
+
sp_sso = root.add_element "md:SPSSODescriptor", {
|
19
|
+
"protocolSupportEnumeration" => "urn:oasis:names:tc:SAML:2.0:protocol",
|
20
|
+
# Metadata request need not be signed (as we don't publish our cert)
|
21
|
+
"AuthnRequestsSigned" => false,
|
22
|
+
# However we would like assertions signed if idp_cert_fingerprint or idp_cert is set
|
23
|
+
"WantAssertionsSigned" => (!settings.idp_cert_fingerprint.nil? || !settings.idp_cert.nil?)
|
24
|
+
}
|
25
|
+
if settings.sp_entity_id != nil
|
26
|
+
root.attributes["entityID"] = settings.sp_entity_id
|
27
|
+
end
|
28
|
+
if settings.single_logout_service_url != nil
|
29
|
+
sp_sso.add_element "md:SingleLogoutService", {
|
30
|
+
# Add this as a setting to create different bindings?
|
31
|
+
"Binding" => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
|
32
|
+
"Location" => settings.single_logout_service_url,
|
33
|
+
"ResponseLocation" => settings.single_logout_service_url,
|
34
|
+
"isDefault" => true,
|
35
|
+
"index" => 0
|
36
|
+
}
|
37
|
+
end
|
38
|
+
if settings.name_identifier_format != nil
|
39
|
+
name_id = sp_sso.add_element "md:NameIDFormat"
|
40
|
+
name_id.text = settings.name_identifier_format
|
41
|
+
end
|
42
|
+
if settings.assertion_consumer_service_url != nil
|
43
|
+
sp_sso.add_element "md:AssertionConsumerService", {
|
44
|
+
# Add this as a setting to create different bindings?
|
45
|
+
"Binding" => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
46
|
+
"Location" => settings.assertion_consumer_service_url,
|
47
|
+
"isDefault" => true,
|
48
|
+
"index" => 0
|
49
|
+
}
|
50
|
+
end
|
51
|
+
# With OpenSSO, it might be required to also include
|
52
|
+
# <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"/>
|
53
|
+
# <md:XACMLAuthzDecisionQueryDescriptor WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"/>
|
54
|
+
|
55
|
+
meta_doc << REXML::XMLDecl.new
|
56
|
+
ret = ""
|
57
|
+
# pretty print the XML so IdP administrators can easily see what the SP supports
|
58
|
+
meta_doc.write(ret, 1)
|
59
|
+
|
60
|
+
Logging.debug "Generated metadata:\n#{ret}"
|
61
|
+
|
62
|
+
ret
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,426 @@
|
|
1
|
+
require "xml_security"
|
2
|
+
require "time"
|
3
|
+
require "nokogiri"
|
4
|
+
require 'onelogin/ruby-saml/attributes'
|
5
|
+
|
6
|
+
# Only supports SAML 2.0
|
7
|
+
module OneLogin
|
8
|
+
module RubySaml
|
9
|
+
|
10
|
+
class Response
|
11
|
+
ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
|
12
|
+
PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
|
13
|
+
DSIG = "http://www.w3.org/2000/09/xmldsig#"
|
14
|
+
|
15
|
+
# TODO: This should probably be ctor initialized too... WDYT?
|
16
|
+
attr_accessor :settings
|
17
|
+
|
18
|
+
attr_reader :options
|
19
|
+
attr_reader :response
|
20
|
+
attr_reader :document
|
21
|
+
|
22
|
+
def initialize(response, options = {})
|
23
|
+
raise ArgumentError.new("Response cannot be nil") if response.nil?
|
24
|
+
@options = options
|
25
|
+
@response = (response =~ /^</) ? response : Base64.decode64(response)
|
26
|
+
@document = XMLSecurity::SignedDocument.new(@response)
|
27
|
+
end
|
28
|
+
|
29
|
+
def is_valid?
|
30
|
+
validate
|
31
|
+
end
|
32
|
+
|
33
|
+
def validate!
|
34
|
+
validate(false)
|
35
|
+
end
|
36
|
+
|
37
|
+
# The value of the user identifier as designated by the initialization request response
|
38
|
+
def name_id
|
39
|
+
@name_id ||= begin
|
40
|
+
node = xpath_first_from_signed_assertion('/a:Subject/a:NameID')
|
41
|
+
Utils.element_text(node)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
alias nameid name_id
|
46
|
+
|
47
|
+
def sessionindex
|
48
|
+
@sessionindex ||= begin
|
49
|
+
node = xpath_first_from_signed_assertion('/a:AuthnStatement')
|
50
|
+
node.nil? ? nil : node.attributes['SessionIndex']
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Gets the Attributes from the AttributeStatement element.
|
55
|
+
#
|
56
|
+
# All attributes can be iterated over +attributes.each+ or returned as array by +attributes.all+
|
57
|
+
# For backwards compatibility ruby-saml returns by default only the first value for a given attribute with
|
58
|
+
# attributes['name']
|
59
|
+
# To get all of the attributes, use:
|
60
|
+
# attributes.multi('name')
|
61
|
+
# Or turn off the compatibility:
|
62
|
+
# OneLogin::RubySaml::Attributes.single_value_compatibility = false
|
63
|
+
# Now this will return an array:
|
64
|
+
# attributes['name']
|
65
|
+
#
|
66
|
+
# @return [Attributes] OneLogin::RubySaml::Attributes enumerable collection.
|
67
|
+
#
|
68
|
+
def attributes
|
69
|
+
@attr_statements ||= begin
|
70
|
+
attributes = Attributes.new
|
71
|
+
|
72
|
+
stmt_elements = xpath_from_signed_assertion('/a:AttributeStatement')
|
73
|
+
stmt_elements.each do |stmt_element|
|
74
|
+
stmt_element.elements.each do |attr_element|
|
75
|
+
name = attr_element.attributes["Name"]
|
76
|
+
values = attr_element.elements.collect{|e|
|
77
|
+
if (e.elements.nil? || e.elements.size == 0)
|
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 : Utils.element_text(e)
|
81
|
+
# explicitly support saml2:NameID with saml2:NameQualifier if supplied in attributes
|
82
|
+
# this is useful for allowing eduPersonTargetedId to be passed as an opaque identifier to use to
|
83
|
+
# identify the subject in an SP rather than email or other less opaque attributes
|
84
|
+
# NameQualifier, if present is prefixed with a "/" to the value
|
85
|
+
else
|
86
|
+
REXML::XPath.match(e,'a:NameID', { "a" => ASSERTION }).collect{|n|
|
87
|
+
(n.attributes['NameQualifier'] ? n.attributes['NameQualifier'] +"/" : '') + Utils.element_text(n)
|
88
|
+
}
|
89
|
+
end
|
90
|
+
}
|
91
|
+
|
92
|
+
attributes.add(name, values.flatten)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
attributes
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# When this user session should expire at latest
|
100
|
+
def session_expires_at
|
101
|
+
@expires_at ||= begin
|
102
|
+
node = xpath_first_from_signed_assertion('/a:AuthnStatement')
|
103
|
+
parse_time(node, "SessionNotOnOrAfter")
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Checks the status of the response for a "Success" code
|
108
|
+
def success?
|
109
|
+
@status_code ||= begin
|
110
|
+
node = REXML::XPath.first(document, "/p:Response/p:Status/p:StatusCode", { "p" => PROTOCOL, "a" => ASSERTION })
|
111
|
+
node.attributes["Value"] == "urn:oasis:names:tc:SAML:2.0:status:Success"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Conditions (if any) for the assertion to run
|
116
|
+
def conditions
|
117
|
+
@conditions ||= xpath_first_from_signed_assertion('/a:Conditions')
|
118
|
+
end
|
119
|
+
|
120
|
+
def not_before
|
121
|
+
@not_before ||= parse_time(conditions, "NotBefore")
|
122
|
+
end
|
123
|
+
|
124
|
+
def not_on_or_after
|
125
|
+
@not_on_or_after ||= parse_time(conditions, "NotOnOrAfter")
|
126
|
+
end
|
127
|
+
|
128
|
+
def issuer
|
129
|
+
@issuer ||= begin
|
130
|
+
node = REXML::XPath.first(document, "/p:Response/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
|
131
|
+
node ||= xpath_first_from_signed_assertion('/a:Issuer')
|
132
|
+
Utils.element_text(node)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# @return [Array] The Audience elements from the Contitions of the SAML Response.
|
137
|
+
#
|
138
|
+
def audiences
|
139
|
+
@audiences ||= begin
|
140
|
+
nodes = xpath_from_signed_assertion('/a:Conditions/a:AudienceRestriction/a:Audience')
|
141
|
+
nodes.map { |node| Utils.element_text(node) }.reject(&:empty?)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
private
|
146
|
+
|
147
|
+
def validation_error(message)
|
148
|
+
raise ValidationError.new(message)
|
149
|
+
end
|
150
|
+
|
151
|
+
def validate(soft = true)
|
152
|
+
validate_structure(soft) &&
|
153
|
+
validate_success_status(soft) &&
|
154
|
+
validate_num_assertion &&
|
155
|
+
validate_signed_elements(soft) &&
|
156
|
+
validate_response_state(soft) &&
|
157
|
+
validate_conditions(soft) &&
|
158
|
+
validate_audience(soft) &&
|
159
|
+
document.validate_document(get_fingerprint, soft) &&
|
160
|
+
success?
|
161
|
+
end
|
162
|
+
|
163
|
+
# Validates that the SAML Response only contains a single Assertion (encrypted or not).
|
164
|
+
# @return [Boolean] True if the SAML Response contains one unique Assertion, otherwise False
|
165
|
+
#
|
166
|
+
def validate_num_assertion(soft = true)
|
167
|
+
assertions = REXML::XPath.match(
|
168
|
+
document,
|
169
|
+
"//a:Assertion",
|
170
|
+
{ "a" => ASSERTION }
|
171
|
+
)
|
172
|
+
encrypted_assertions = REXML::XPath.match(
|
173
|
+
document,
|
174
|
+
"//a:EncryptedAssertion",
|
175
|
+
{ "a" => ASSERTION }
|
176
|
+
)
|
177
|
+
|
178
|
+
unless assertions.size + encrypted_assertions.size == 1
|
179
|
+
return soft ? false : validation_error("SAML Response must contain 1 assertion")
|
180
|
+
end
|
181
|
+
|
182
|
+
true
|
183
|
+
end
|
184
|
+
|
185
|
+
# Validates the Signed elements
|
186
|
+
# @return [Boolean] True if there is 1 or 2 Elements signed in the SAML Response
|
187
|
+
# an are a Response or an Assertion Element, otherwise False if soft=True
|
188
|
+
#
|
189
|
+
def validate_signed_elements(soft)
|
190
|
+
signature_nodes = REXML::XPath.match(
|
191
|
+
document,
|
192
|
+
"//ds:Signature",
|
193
|
+
{"ds"=>DSIG}
|
194
|
+
)
|
195
|
+
signed_elements = []
|
196
|
+
verified_seis = []
|
197
|
+
verified_ids = []
|
198
|
+
signature_nodes.each do |signature_node|
|
199
|
+
signed_element = signature_node.parent.name
|
200
|
+
if signed_element != 'Response' && signed_element != 'Assertion'
|
201
|
+
return soft ? false : validation_error("Invalid Signature Element '#{signed_element}'. SAML Response rejected")
|
202
|
+
end
|
203
|
+
|
204
|
+
if signature_node.parent.attributes['ID'].nil?
|
205
|
+
return soft ? false : validation_error("Signed Element must contain an ID. SAML Response rejected")
|
206
|
+
end
|
207
|
+
|
208
|
+
id = signature_node.parent.attributes.get_attribute("ID").value
|
209
|
+
if verified_ids.include?(id)
|
210
|
+
return soft ? false : validation_error("Duplicated ID. SAML Response rejected")
|
211
|
+
end
|
212
|
+
verified_ids.push(id)
|
213
|
+
|
214
|
+
# Check that reference URI matches the parent ID and no duplicate References or IDs
|
215
|
+
ref = REXML::XPath.first(signature_node, ".//ds:Reference", {"ds"=>DSIG})
|
216
|
+
if ref
|
217
|
+
uri = ref.attributes.get_attribute("URI")
|
218
|
+
if uri && !uri.value.empty?
|
219
|
+
sei = uri.value[1..-1]
|
220
|
+
|
221
|
+
unless sei == id
|
222
|
+
return soft ? false : validation_error("Found an invalid Signed Element. SAML Response rejected")
|
223
|
+
end
|
224
|
+
|
225
|
+
if verified_seis.include?(sei)
|
226
|
+
return soft ? false : validation_error("Duplicated Reference URI. SAML Response rejected")
|
227
|
+
return append_error("Duplicated Reference URI. SAML Response rejected")
|
228
|
+
end
|
229
|
+
|
230
|
+
verified_seis.push(sei)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
signed_elements << signed_element
|
235
|
+
end
|
236
|
+
|
237
|
+
unless signature_nodes.length < 3 && !signed_elements.empty?
|
238
|
+
return soft ? false : validation_error("Found an unexpected number of Signature Element. SAML Response rejected")
|
239
|
+
end
|
240
|
+
|
241
|
+
true
|
242
|
+
end
|
243
|
+
|
244
|
+
# Validates the Status of the SAML Response
|
245
|
+
# @return [Boolean] True if the SAML Response contains a Success code, otherwise False if soft == false
|
246
|
+
# @raise [ValidationError] if soft == false and validation fails
|
247
|
+
#
|
248
|
+
def validate_success_status(soft = true)
|
249
|
+
return true if success?
|
250
|
+
|
251
|
+
return false unless soft
|
252
|
+
|
253
|
+
error_msg = 'The status code of the Response was not Success'
|
254
|
+
status_error_msg = OneLogin::RubySaml::Utils.status_error_msg(error_msg, status_code, status_message)
|
255
|
+
return validation_error(status_error_msg)
|
256
|
+
end
|
257
|
+
|
258
|
+
# Checks if the Status has the "Success" code
|
259
|
+
# @return [Boolean] True if the StatusCode is Sucess
|
260
|
+
#
|
261
|
+
def success?
|
262
|
+
status_code == "urn:oasis:names:tc:SAML:2.0:status:Success"
|
263
|
+
end
|
264
|
+
|
265
|
+
# @return [String] StatusCode value from a SAML Response.
|
266
|
+
#
|
267
|
+
def status_code
|
268
|
+
@status_code ||= begin
|
269
|
+
nodes = REXML::XPath.match(
|
270
|
+
document,
|
271
|
+
"/p:Response/p:Status/p:StatusCode",
|
272
|
+
{ "p" => PROTOCOL }
|
273
|
+
)
|
274
|
+
if nodes.size == 1
|
275
|
+
node = nodes[0]
|
276
|
+
code = node.attributes["Value"] if node && node.attributes
|
277
|
+
|
278
|
+
unless code == "urn:oasis:names:tc:SAML:2.0:status:Success"
|
279
|
+
nodes = REXML::XPath.match(
|
280
|
+
document,
|
281
|
+
"/p:Response/p:Status/p:StatusCode/p:StatusCode",
|
282
|
+
{ "p" => PROTOCOL }
|
283
|
+
)
|
284
|
+
statuses = nodes.collect do |inner_node|
|
285
|
+
inner_node.attributes["Value"]
|
286
|
+
end
|
287
|
+
extra_code = statuses.join(" | ")
|
288
|
+
if extra_code
|
289
|
+
code = "#{code} | #{extra_code}"
|
290
|
+
end
|
291
|
+
end
|
292
|
+
code
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
# @return [String] the StatusMessage value from a SAML Response.
|
298
|
+
#
|
299
|
+
def status_message
|
300
|
+
@status_message ||= begin
|
301
|
+
nodes = REXML::XPath.match(
|
302
|
+
document,
|
303
|
+
"/p:Response/p:Status/p:StatusMessage",
|
304
|
+
{ "p" => PROTOCOL }
|
305
|
+
)
|
306
|
+
if nodes.size == 1
|
307
|
+
Utils.element_text(nodes.first)
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
def validate_structure(soft = true)
|
313
|
+
Dir.chdir(File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'schemas'))) do
|
314
|
+
@schema = Nokogiri::XML::Schema(IO.read('saml20protocol_schema.xsd'))
|
315
|
+
@xml = Nokogiri::XML(self.document.to_s)
|
316
|
+
end
|
317
|
+
if soft
|
318
|
+
@schema.validate(@xml).map{ return false }
|
319
|
+
else
|
320
|
+
@schema.validate(@xml).map{ |error| validation_error("#{error.message}\n\n#{@xml.to_s}") }
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
def validate_response_state(soft = true)
|
325
|
+
if response.empty?
|
326
|
+
return soft ? false : validation_error("Blank response")
|
327
|
+
end
|
328
|
+
|
329
|
+
if settings.nil?
|
330
|
+
return soft ? false : validation_error("No settings on response")
|
331
|
+
end
|
332
|
+
|
333
|
+
if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil?
|
334
|
+
return soft ? false : validation_error("No fingerprint or certificate on settings")
|
335
|
+
end
|
336
|
+
|
337
|
+
true
|
338
|
+
end
|
339
|
+
|
340
|
+
def xpath_first_from_signed_assertion(subelt=nil)
|
341
|
+
node = REXML::XPath.first(
|
342
|
+
document,
|
343
|
+
"/p:Response/a:Assertion[@ID=$id]#{subelt}",
|
344
|
+
{ "p" => PROTOCOL, "a" => ASSERTION },
|
345
|
+
{ 'id' => document.signed_element_id }
|
346
|
+
)
|
347
|
+
node ||= REXML::XPath.first(
|
348
|
+
document,
|
349
|
+
"/p:Response[@ID=$id]/a:Assertion#{subelt}",
|
350
|
+
{ "p" => PROTOCOL, "a" => ASSERTION },
|
351
|
+
{ 'id' => document.signed_element_id }
|
352
|
+
)
|
353
|
+
node
|
354
|
+
end
|
355
|
+
|
356
|
+
# Extracts all the appearances that matchs the subelt (pattern)
|
357
|
+
# Search on any Assertion that is signed, or has a Response parent signed
|
358
|
+
# @param subelt [String] The XPath pattern
|
359
|
+
# @return [Array of REXML::Element] Return all matches
|
360
|
+
#
|
361
|
+
def xpath_from_signed_assertion(subelt=nil)
|
362
|
+
node = REXML::XPath.match(
|
363
|
+
document,
|
364
|
+
"/p:Response/a:Assertion[@ID=$id]#{subelt}",
|
365
|
+
{ "p" => PROTOCOL, "a" => ASSERTION },
|
366
|
+
{ 'id' => document.signed_element_id }
|
367
|
+
)
|
368
|
+
node.concat( REXML::XPath.match(
|
369
|
+
document,
|
370
|
+
"/p:Response[@ID=$id]/a:Assertion#{subelt}",
|
371
|
+
{ "p" => PROTOCOL, "a" => ASSERTION },
|
372
|
+
{ 'id' => document.signed_element_id }
|
373
|
+
))
|
374
|
+
end
|
375
|
+
|
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
|
+
def validate_conditions(soft = true)
|
386
|
+
return true if conditions.nil?
|
387
|
+
return true if options[:skip_conditions]
|
388
|
+
|
389
|
+
now = Time.now.utc
|
390
|
+
|
391
|
+
if not_before && (now + (options[:allowed_clock_drift] || 0)) < not_before
|
392
|
+
return soft ? false : validation_error("Current time is earlier than NotBefore condition")
|
393
|
+
end
|
394
|
+
|
395
|
+
if not_on_or_after && now >= not_on_or_after
|
396
|
+
return soft ? false : validation_error("Current time is on or after NotOnOrAfter condition")
|
397
|
+
end
|
398
|
+
|
399
|
+
true
|
400
|
+
end
|
401
|
+
|
402
|
+
def parse_time(node, attribute)
|
403
|
+
if node && node.attributes[attribute]
|
404
|
+
Time.parse(node.attributes[attribute])
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
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
|
+
def validate_audience(soft = true)
|
414
|
+
return true if audiences.empty? || settings.sp_entity_id.nil? || settings.sp_entity_id.empty?
|
415
|
+
|
416
|
+
unless audiences.include? settings.sp_entity_id
|
417
|
+
s = audiences.count > 1 ? 's' : '';
|
418
|
+
error_msg = "Invalid Audience#{s}. The audience#{s} #{audiences.join(',')}, did not match the expected audience #{settings.sp_entity_id}"
|
419
|
+
return soft ? false : validation_error(error_msg)
|
420
|
+
end
|
421
|
+
|
422
|
+
true
|
423
|
+
end
|
424
|
+
end
|
425
|
+
end
|
426
|
+
end
|