ciam-es 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/Gemfile +4 -0
- data/README.md +127 -0
- data/ciam-es.gemspec +23 -0
- data/lib/ciam-es.rb +14 -0
- data/lib/ciam/ruby-saml/authrequest.rb +206 -0
- data/lib/ciam/ruby-saml/coding.rb +34 -0
- data/lib/ciam/ruby-saml/error_handling.rb +27 -0
- data/lib/ciam/ruby-saml/logging.rb +26 -0
- data/lib/ciam/ruby-saml/logout_request.rb +126 -0
- data/lib/ciam/ruby-saml/logout_response.rb +132 -0
- data/lib/ciam/ruby-saml/metadata.rb +509 -0
- data/lib/ciam/ruby-saml/request.rb +81 -0
- data/lib/ciam/ruby-saml/response.rb +683 -0
- data/lib/ciam/ruby-saml/settings.rb +89 -0
- data/lib/ciam/ruby-saml/utils.rb +225 -0
- data/lib/ciam/ruby-saml/validation_error.rb +7 -0
- data/lib/ciam/ruby-saml/version.rb +5 -0
- data/lib/ciam/xml_security.rb +166 -0
- data/lib/ciam/xml_security_new.rb +373 -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/test/certificates/certificate1 +12 -0
- data/test/logoutrequest_test.rb +98 -0
- data/test/request_test.rb +53 -0
- data/test/response_test.rb +219 -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/no_signature_ns.xml +48 -0
- data/test/responses/open_saml_response.xml +56 -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_with_ampersands.xml +139 -0
- data/test/responses/response_with_ampersands.xml.base64 +93 -0
- data/test/responses/simple_saml_php.xml +71 -0
- data/test/responses/wrapped_response_2.xml.base64 +150 -0
- data/test/settings_test.rb +43 -0
- data/test/test_helper.rb +65 -0
- data/test/xml_security_test.rb +123 -0
- metadata +145 -0
@@ -0,0 +1,81 @@
|
|
1
|
+
|
2
|
+
# A few helper functions for assembling a SAMLRequest and
|
3
|
+
# sending it to the IdP
|
4
|
+
module Ciam::Saml
|
5
|
+
include Coding
|
6
|
+
module Request
|
7
|
+
|
8
|
+
# a few symbols for SAML class names
|
9
|
+
HTTP_POST = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
|
10
|
+
HTTP_GET = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
|
11
|
+
# get the IdP metadata, and select the appropriate SSO binding
|
12
|
+
# that we can support. Currently this is HTTP-Redirect and HTTP-POST
|
13
|
+
# but more could be added in the future
|
14
|
+
def binding_select(service)
|
15
|
+
# first check if we're still using the old hard coded method for
|
16
|
+
# backwards compatability
|
17
|
+
if @settings.idp_metadata == nil && @settings.idp_sso_target_url != nil
|
18
|
+
@URL = @settings.idp_sso_target_url
|
19
|
+
return "GET", content_get
|
20
|
+
end
|
21
|
+
# grab the metadata
|
22
|
+
metadata = Metadata::new
|
23
|
+
meta_doc = metadata.get_idp_metadata(@settings)
|
24
|
+
|
25
|
+
# first try POST
|
26
|
+
sso_element = REXML::XPath.first(meta_doc,
|
27
|
+
"/EntityDescriptor/IDPSSODescriptor/#{service}[@Binding='#{HTTP_POST}']")
|
28
|
+
if sso_element
|
29
|
+
@URL = sso_element.attributes["Location"]
|
30
|
+
#Logging.debug "binding_select: POST to #{@URL}"
|
31
|
+
return "POST", content_post
|
32
|
+
end
|
33
|
+
|
34
|
+
# next try GET
|
35
|
+
sso_element = REXML::XPath.first(meta_doc,
|
36
|
+
"/EntityDescriptor/IDPSSODescriptor/#{service}[@Binding='#{HTTP_GET}']")
|
37
|
+
if sso_element
|
38
|
+
@URL = sso_element.attributes["Location"]
|
39
|
+
Logging.debug "binding_select: GET from #{@URL}"
|
40
|
+
return "GET", content_get
|
41
|
+
end
|
42
|
+
# other types we might want to add in the future: SOAP, Artifact
|
43
|
+
end
|
44
|
+
|
45
|
+
# construct the the parameter list on the URL and return
|
46
|
+
def content_get
|
47
|
+
# compress GET requests to try and stay under that 8KB request limit
|
48
|
+
deflated_request = Zlib::Deflate.deflate(@request, 9)[2..-5]
|
49
|
+
# strict_encode64() isn't available? sub out the newlines
|
50
|
+
@request_params["SAMLRequest"] = Base64.encode64(deflated_request).gsub(/\n/, "")
|
51
|
+
|
52
|
+
Logging.debug "SAMLRequest=#{@request_params["SAMLRequest"]}"
|
53
|
+
uri = Addressable::URI.parse(@URL)
|
54
|
+
uri.query_values = @request_params
|
55
|
+
url = uri.to_s
|
56
|
+
#url = @URL + "?SAMLRequest=" + @request_params["SAMLRequest"]
|
57
|
+
Logging.debug "Sending to URL #{url}"
|
58
|
+
return url
|
59
|
+
end
|
60
|
+
# construct an HTML form (POST) and return the content
|
61
|
+
def content_post
|
62
|
+
# POST requests seem to bomb out when they're deflated
|
63
|
+
# and they probably don't need to be compressed anyway
|
64
|
+
@request_params["SAMLRequest"] = Base64.encode64(@request).gsub(/\n/, "")
|
65
|
+
|
66
|
+
#Logging.debug "SAMLRequest=#{@request_params["SAMLRequest"]}"
|
67
|
+
# kind of a cheesy method of building an HTML, form since we can't rely on Rails too much,
|
68
|
+
# and REXML doesn't work well with quote characters
|
69
|
+
str = "<html><body onLoad=\"document.getElementById('form').submit();\">\n"
|
70
|
+
str += "<form id='form' name='form' method='POST' action=\"#{@URL}\">\n"
|
71
|
+
# we could change this in the future to associate a temp auth session ID
|
72
|
+
str += "<input name='RelayState' value='ruby-saml' type='hidden' />\n"
|
73
|
+
@request_params.each_pair do |key, value|
|
74
|
+
str += "<input name=\"#{key}\" value=\"#{value}\" type='hidden' />\n"
|
75
|
+
end
|
76
|
+
str += "</form></body></html>\n"
|
77
|
+
#Logging.debug "Created form:\n#{str}"
|
78
|
+
return str
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,683 @@
|
|
1
|
+
require_relative "../xml_security_new"
|
2
|
+
require "time"
|
3
|
+
require "nokogiri"
|
4
|
+
require "base64"
|
5
|
+
require "openssl"
|
6
|
+
require "digest/sha1"
|
7
|
+
require_relative "utils"
|
8
|
+
|
9
|
+
# Only supports SAML 2.0
|
10
|
+
module Ciam
|
11
|
+
module Saml
|
12
|
+
|
13
|
+
class Response
|
14
|
+
ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
|
15
|
+
PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
|
16
|
+
DSIG = "http://www.w3.org/2000/09/xmldsig#"
|
17
|
+
|
18
|
+
attr_accessor :options, :response, :document, :settings, :attr_name_format
|
19
|
+
attr_reader :decrypted_document
|
20
|
+
|
21
|
+
|
22
|
+
def initialize(response, options = {})
|
23
|
+
raise ArgumentError.new("Response cannot be nil") if response.nil?
|
24
|
+
self.options = options
|
25
|
+
self.response = response
|
26
|
+
if assertion_encrypted?
|
27
|
+
@decrypted_document = generate_decrypted_document
|
28
|
+
end
|
29
|
+
begin
|
30
|
+
self.document = Ciam::XMLSecurityNew::SignedDocument.new(Base64.decode64(response))
|
31
|
+
rescue REXML::ParseException => e
|
32
|
+
if response =~ /</
|
33
|
+
self.document = Ciam::XMLSecurityNew::SignedDocument.new(response)
|
34
|
+
else
|
35
|
+
raise e
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
# Checks if the SAML Response contains or not an EncryptedAssertion element
|
42
|
+
# @return [Boolean] True if the SAML Response contains an EncryptedAssertion element
|
43
|
+
#
|
44
|
+
def assertion_encrypted?
|
45
|
+
false
|
46
|
+
#!REXML::XPath.first(self.document, "(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)", { "p" => PROTOCOL, "a" => ASSERTION }).nil?
|
47
|
+
end
|
48
|
+
|
49
|
+
def is_valid?
|
50
|
+
validate
|
51
|
+
end
|
52
|
+
|
53
|
+
def validate!
|
54
|
+
validate(false)
|
55
|
+
end
|
56
|
+
|
57
|
+
# The value of the user identifier as designated by the initialization request response
|
58
|
+
def name_id
|
59
|
+
@name_id ||= begin
|
60
|
+
node = REXML::XPath.first(document, "/saml2p:Response/saml2:Assertion[@ID='#{document.signed_element_id}']/saml2:Subject/saml2:NameID")
|
61
|
+
node ||= REXML::XPath.first(document, "/saml2p:Response[@ID='#{document.signed_element_id}']/saml2:Assertion/saml2:Subject/saml2:NameID")
|
62
|
+
node.nil? ? nil : node.text
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
|
68
|
+
|
69
|
+
# A hash of alle the attributes with the response. Assuming there is only one value for each key
|
70
|
+
def attributes
|
71
|
+
@attr_statements ||= begin
|
72
|
+
result = {}
|
73
|
+
stmt_element = REXML::XPath.first(document, "/p:Response/a:Assertion/a:AttributeStatement", { "p" => PROTOCOL, "a" => ASSERTION })
|
74
|
+
return {} if stmt_element.nil?
|
75
|
+
|
76
|
+
@attr_name_format = []
|
77
|
+
stmt_element.elements.each do |attr_element|
|
78
|
+
name = attr_element.attributes["Name"]
|
79
|
+
#salvo i vari format per controllare poi che non ce ne siano di null
|
80
|
+
@attr_name_format << attr_element.attributes["NameFormat"].blank? ? nil : attr_element.attributes["NameFormat"].text
|
81
|
+
value = (attr_element.elements.blank? ? nil : attr_element.elements.first.text)
|
82
|
+
|
83
|
+
result[name] = value
|
84
|
+
end
|
85
|
+
#mette il symbol
|
86
|
+
result.keys.each do |key|
|
87
|
+
result[key.intern] = result[key]
|
88
|
+
end
|
89
|
+
|
90
|
+
result
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# When this user session should expire at latest
|
95
|
+
def session_expires_at
|
96
|
+
@expires_at ||= begin
|
97
|
+
node = REXML::XPath.first(document, "/p:Response/a:Assertion/a:AuthnStatement", { "p" => PROTOCOL, "a" => ASSERTION })
|
98
|
+
parse_time(node, "SessionNotOnOrAfter")
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
|
104
|
+
# Checks the status of the response for a "Success" code
|
105
|
+
def success?
|
106
|
+
@status_code ||= begin
|
107
|
+
node = REXML::XPath.first(document, "/p:Response/p:Status/p:StatusCode", { "p" => PROTOCOL, "a" => ASSERTION })
|
108
|
+
node.attributes["Value"] == "urn:oasis:names:tc:SAML:2.0:status:Success" unless node.blank?
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Ritorno il valore dello StatusMessage
|
113
|
+
def get_status_message
|
114
|
+
node = REXML::XPath.first(document, "/p:Response/p:Status/p:StatusMessage", { "p" => PROTOCOL, "a" => ASSERTION })
|
115
|
+
node.text unless node.blank?
|
116
|
+
end
|
117
|
+
|
118
|
+
# Conditions (if any) for the assertion to run
|
119
|
+
def conditions
|
120
|
+
@conditions ||= begin
|
121
|
+
REXML::XPath.first(document, "/p:Response/a:Assertion[@ID='#{document.signed_element_id}']/a:Conditions", { "p" => PROTOCOL, "a" => ASSERTION })
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
|
127
|
+
#metodi per ricavare info per tracciatura agid
|
128
|
+
|
129
|
+
|
130
|
+
def issuer
|
131
|
+
@issuer ||= begin
|
132
|
+
node = REXML::XPath.first(document, "/p:Response/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
|
133
|
+
node ||= REXML::XPath.first(document, "/p:Response/a:Assertion/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
|
134
|
+
node.nil? ? nil : node.text
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Gets the Issuers (from Response and Assertion).
|
139
|
+
# (returns the first node that matches the supplied xpath from the Response and from the Assertion)
|
140
|
+
# @return [Array] Array with the Issuers (REXML::Element)
|
141
|
+
#
|
142
|
+
def issuers(soft=true)
|
143
|
+
@issuers ||= begin
|
144
|
+
issuer_response_nodes = REXML::XPath.match(
|
145
|
+
document,
|
146
|
+
"/p:Response/a:Issuer",
|
147
|
+
{ "p" => PROTOCOL, "a" => ASSERTION }
|
148
|
+
)
|
149
|
+
|
150
|
+
unless issuer_response_nodes.size == 1
|
151
|
+
# error_msg = "Issuer of the Response not found or multiple."
|
152
|
+
# raise ValidationError.new(error_msg)
|
153
|
+
return (soft ? false : validation_error("Issuer of the Response not found or multiple."))
|
154
|
+
end
|
155
|
+
|
156
|
+
issuer_assertion_nodes = xpath_from_signed_assertion("/a:Issuer")
|
157
|
+
unless issuer_assertion_nodes.size == 1
|
158
|
+
# error_msg = "Issuer of the Assertion not found or multiple."
|
159
|
+
# raise ValidationError.new(error_msg)
|
160
|
+
return (soft ? false : validation_error("Issuer of the Assertion not found or multiple."))
|
161
|
+
end
|
162
|
+
|
163
|
+
issuer_response_nodes.each{ |iss|
|
164
|
+
#controllo: L'attributo Format di Issuer deve essere presente con il valore urn:oasis:names:tc:SAML:2.0:nameid-format:entity
|
165
|
+
return (soft ? false : validation_error("Elemento Issuer non ha formato corretto ")) if !iss.attributes['Format'].nil? && iss.attributes['Format'] != 'urn:oasis:names:tc:SAML:2.0:nameid-format:entity'
|
166
|
+
|
167
|
+
}
|
168
|
+
|
169
|
+
issuer_assertion_nodes.each{ |iss|
|
170
|
+
#controllo: L'attributo Format di Issuer deve essere presente con il valore urn:oasis:names:tc:SAML:2.0:nameid-format:entity
|
171
|
+
return (soft ? false : validation_error("Elemento Issuer non ha formato corretto ")) if iss.attributes['Format'] != 'urn:oasis:names:tc:SAML:2.0:nameid-format:entity'
|
172
|
+
|
173
|
+
}
|
174
|
+
|
175
|
+
nodes = issuer_response_nodes + issuer_assertion_nodes
|
176
|
+
|
177
|
+
nodes.map { |node| Utils.element_text(node) }.compact.uniq
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
|
182
|
+
|
183
|
+
def response_to_id
|
184
|
+
node = REXML::XPath.first(document, "/p:Response", { "p" => PROTOCOL })
|
185
|
+
return node.attributes["InResponseTo"] unless node.blank?
|
186
|
+
end
|
187
|
+
|
188
|
+
def id
|
189
|
+
node = REXML::XPath.first(document, "/p:Response", { "p" => PROTOCOL })
|
190
|
+
return node.attributes["ID"] unless node.blank?
|
191
|
+
end
|
192
|
+
|
193
|
+
def issue_instant
|
194
|
+
node = REXML::XPath.first(document, "/p:Response", { "p" => PROTOCOL })
|
195
|
+
return node.attributes["IssueInstant"] unless node.blank?
|
196
|
+
end
|
197
|
+
|
198
|
+
def assertion_present?
|
199
|
+
node = REXML::XPath.first(document, "/p:Response/a:Assertion/", { "p" => PROTOCOL, "a" => ASSERTION })
|
200
|
+
return !node.blank?
|
201
|
+
end
|
202
|
+
|
203
|
+
def assertion_issue_instant
|
204
|
+
node = REXML::XPath.first(document, "/p:Response/a:Assertion/", { "p" => PROTOCOL, "a" => ASSERTION })
|
205
|
+
return node.attributes["IssueInstant"] unless node.blank?
|
206
|
+
end
|
207
|
+
|
208
|
+
def assertion_id
|
209
|
+
node = REXML::XPath.first(document, "/p:Response/a:Assertion/", { "p" => PROTOCOL, "a" => ASSERTION })
|
210
|
+
return node.attributes["ID"] unless node.blank?
|
211
|
+
end
|
212
|
+
|
213
|
+
def assertion_subject
|
214
|
+
node = REXML::XPath.first(document, "/p:Response/a:Assertion/a:Subject/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION })
|
215
|
+
return node.text
|
216
|
+
end
|
217
|
+
|
218
|
+
def assertion_subject_name_qualifier
|
219
|
+
node = REXML::XPath.first(document, "/p:Response/a:Assertion/a:Subject/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION })
|
220
|
+
return node.attributes["NameQualifier"] unless node.blank?
|
221
|
+
end
|
222
|
+
|
223
|
+
def assertion_subject_confirmation_data_not_on_or_after
|
224
|
+
node_subj_conf_data = xpath_first_from_signed_assertion('/a:Subject/a:SubjectConfirmation/a:SubjectConfirmationData')
|
225
|
+
return node_subj_conf_data.attributes["NotOnOrAfter"] unless node_subj_conf_data.blank?
|
226
|
+
end
|
227
|
+
|
228
|
+
def assertion_conditions_not_before
|
229
|
+
node_cond_not_before = xpath_first_from_signed_assertion('/a:Conditions')
|
230
|
+
return node_cond_not_before.attributes["NotBefore"] unless node_cond_not_before.blank?
|
231
|
+
end
|
232
|
+
|
233
|
+
def assertion_conditions_not_on_or_after
|
234
|
+
node_cond_not_on_or_after = xpath_first_from_signed_assertion('/a:Conditions')
|
235
|
+
return node_cond_not_on_or_after.attributes["NotOnOrAfter"] unless node_cond_not_on_or_after.blank?
|
236
|
+
end
|
237
|
+
|
238
|
+
private
|
239
|
+
|
240
|
+
def validation_error(message)
|
241
|
+
raise ValidationError.new(message)
|
242
|
+
end
|
243
|
+
|
244
|
+
def validate(soft = true)
|
245
|
+
# prime the IdP metadata before the document validation.
|
246
|
+
# The idp_cert needs to be populated before the validate_response_state method
|
247
|
+
|
248
|
+
if settings
|
249
|
+
idp_metadata = Ciam::Saml::Metadata.new(settings).get_idp_metadata
|
250
|
+
end
|
251
|
+
|
252
|
+
#carico nei setting l'idp_entity_id
|
253
|
+
entity_descriptor_element = REXML::XPath.first(idp_metadata,"/EntityDescriptor")
|
254
|
+
if !entity_descriptor_element.nil?
|
255
|
+
settings.idp_entity_id = entity_descriptor_element.attributes["entityID"]
|
256
|
+
end
|
257
|
+
|
258
|
+
return false if validate_structure(soft) == false
|
259
|
+
return false if validate_response_state(soft) == false
|
260
|
+
return false if validate_conditions(soft) == false
|
261
|
+
#validazione assertion firmata
|
262
|
+
return false if validate_signed_elements(soft) == false
|
263
|
+
#validazione version che sia 2.0
|
264
|
+
return false if validate_version(soft) == false
|
265
|
+
#validazione version delle asserzioni che sia 2.0
|
266
|
+
return false if validate_version_assertion(soft) == false
|
267
|
+
#validazione destination
|
268
|
+
return false if validate_destination(soft) == false
|
269
|
+
#validazione status
|
270
|
+
return false if validate_status(soft) == false
|
271
|
+
#validazione inresponseto
|
272
|
+
return false if validate_presence_inresponseto(soft) == false
|
273
|
+
#validazione issuer
|
274
|
+
return false if validate_issuer(soft) == false
|
275
|
+
#validazioni varie su asserzioni
|
276
|
+
return false if validate_assertion(soft) == false
|
277
|
+
#validazione presenza format su attributes
|
278
|
+
return false if validate_name_format_attributes(soft) == false
|
279
|
+
|
280
|
+
|
281
|
+
# Just in case a user needs to toss out the signature validation,
|
282
|
+
# I'm adding in an option for it. (Sometimes canonicalization is a bitch!)
|
283
|
+
return true if settings.skip_validation == true
|
284
|
+
|
285
|
+
# document.validte populates the idp_cert
|
286
|
+
return false if document.validate_document(get_fingerprint, soft) == false
|
287
|
+
|
288
|
+
# validate response code
|
289
|
+
return false if success? == false
|
290
|
+
|
291
|
+
return true
|
292
|
+
end
|
293
|
+
|
294
|
+
# Validates the Issuer (Of the SAML Response and the SAML Assertion)
|
295
|
+
# @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the response is invalid or not)
|
296
|
+
# @return [Boolean] True if the Issuer matchs the IdP entityId, otherwise False if soft=True
|
297
|
+
# @raise [ValidationError] if soft == false and validation fails
|
298
|
+
#
|
299
|
+
def validate_issuer(soft=true)
|
300
|
+
obtained_issuers = issuers(soft)
|
301
|
+
if obtained_issuers == false
|
302
|
+
return false #errori all'interno del metodo issuers
|
303
|
+
else
|
304
|
+
obtained_issuers.each do |iss|
|
305
|
+
|
306
|
+
unless Ciam::Saml::Utils.uri_match?(iss, settings.idp_entity_id)
|
307
|
+
# error_msg = "Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>"
|
308
|
+
# return append_error(error_msg)
|
309
|
+
return (soft ? false : validation_error("Elemento Issuer diverso da EntityID IdP, expected: <#{settings.idp_entity_id}>, but was: <#{iss}>"))
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
true
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
def validate_presence_inresponseto(soft=true)
|
318
|
+
response_to_id_value = response_to_id
|
319
|
+
return (soft ? false : validation_error("InResponseTo non specificato o mancante")) if response_to_id_value.blank?
|
320
|
+
end
|
321
|
+
|
322
|
+
|
323
|
+
|
324
|
+
#validate status e status code
|
325
|
+
def validate_status(soft=true)
|
326
|
+
#controlli su status
|
327
|
+
node_status = REXML::XPath.first(document, "/p:Response/p:Status", { "p" => PROTOCOL, "a" => ASSERTION })
|
328
|
+
return (soft ? false : validation_error("Status non presente")) if node_status.blank?
|
329
|
+
#controlli su status code
|
330
|
+
node_status_code = REXML::XPath.first(document, "/p:Response/p:Status/p:StatusCode", { "p" => PROTOCOL, "a" => ASSERTION })
|
331
|
+
return (soft ? false : validation_error("Status code non presente")) if node_status_code.blank?
|
332
|
+
return (soft ? false : validation_error("Status non presente")) unless node_status_code.attributes["Value"] == "urn:oasis:names:tc:SAML:2.0:status:Success"
|
333
|
+
true
|
334
|
+
end
|
335
|
+
|
336
|
+
|
337
|
+
|
338
|
+
# Validates the SAML version (2.0)
|
339
|
+
# If fails, the error is added to the errors array.
|
340
|
+
# @return [Boolean] True if the SAML Response is 2.0, otherwise returns False
|
341
|
+
#
|
342
|
+
def version(document)
|
343
|
+
@version ||= begin
|
344
|
+
node = REXML::XPath.first(
|
345
|
+
document,
|
346
|
+
"/p:AuthnRequest | /p:Response | /p:LogoutResponse | /p:LogoutRequest",
|
347
|
+
{ "p" => PROTOCOL }
|
348
|
+
)
|
349
|
+
node.nil? ? nil : node.attributes['Version']
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
def version_assertion(document)
|
354
|
+
assertion_nodes = xpath_from_signed_assertion()
|
355
|
+
@version_assertion = "2.0"
|
356
|
+
#ciclo sui nodi delle asserzioni, se uno ha una versione diversa da 2.0 ritorno nil
|
357
|
+
unless assertion_nodes.blank?
|
358
|
+
assertion_nodes.each{ |ass_node|
|
359
|
+
return nil if ass_node.attributes['Version'] != "2.0"
|
360
|
+
}
|
361
|
+
end
|
362
|
+
@version_assertion
|
363
|
+
end
|
364
|
+
|
365
|
+
def validate_version(soft = true)
|
366
|
+
unless version(self.document) == "2.0"
|
367
|
+
#return append_error("Unsupported SAML version")
|
368
|
+
return soft ? false : validation_error("Unsupported SAML version")
|
369
|
+
end
|
370
|
+
true
|
371
|
+
end
|
372
|
+
|
373
|
+
def validate_version_assertion(soft = true)
|
374
|
+
unless version_assertion(self.document) == "2.0"
|
375
|
+
#return append_error("Unsupported SAML version")
|
376
|
+
return soft ? false : validation_error("Unsupported SAML Assertion version")
|
377
|
+
end
|
378
|
+
true
|
379
|
+
end
|
380
|
+
|
381
|
+
def validate_signed_elements(soft = true)
|
382
|
+
signature_nodes = REXML::XPath.match(decrypted_document.nil? ? document : decrypted_document,"//ds:Signature",{"ds"=>DSIG})
|
383
|
+
signed_elements = []
|
384
|
+
verified_seis = []
|
385
|
+
verified_ids = []
|
386
|
+
signature_nodes.each do |signature_node|
|
387
|
+
signed_element = signature_node.parent.name
|
388
|
+
if signed_element != 'Response' && signed_element != 'Assertion'
|
389
|
+
return soft ? false : validation_error("Invalid Signature Element '#{signed_element}'. SAML Response rejected")
|
390
|
+
#return append_error("Invalid Signature Element '#{signed_element}'. SAML Response rejected")
|
391
|
+
end
|
392
|
+
|
393
|
+
if signature_node.parent.attributes['ID'].nil?
|
394
|
+
return soft ? false : validation_error("Signed Element must contain an ID. SAML Response rejected")
|
395
|
+
#return append_error("Signed Element must contain an ID. SAML Response rejected")
|
396
|
+
end
|
397
|
+
|
398
|
+
id = signature_node.parent.attributes.get_attribute("ID").value
|
399
|
+
if verified_ids.include?(id)
|
400
|
+
return soft ? false : validation_error("Duplicated ID. SAML Response rejected")
|
401
|
+
#return append_error("Duplicated ID. SAML Response rejected")
|
402
|
+
end
|
403
|
+
verified_ids.push(id)
|
404
|
+
|
405
|
+
# Check that reference URI matches the parent ID and no duplicate References or IDs
|
406
|
+
ref = REXML::XPath.first(signature_node, ".//ds:Reference", {"ds"=>DSIG})
|
407
|
+
if ref
|
408
|
+
uri = ref.attributes.get_attribute("URI")
|
409
|
+
if uri && !uri.value.empty?
|
410
|
+
sei = uri.value[1..-1]
|
411
|
+
|
412
|
+
unless sei == id
|
413
|
+
#return append_error("Found an invalid Signed Element. SAML Response rejected")
|
414
|
+
return soft ? false : validation_error("Found an invalid Signed Element. SAML Response rejected")
|
415
|
+
end
|
416
|
+
|
417
|
+
if verified_seis.include?(sei)
|
418
|
+
#return append_error("Duplicated Reference URI. SAML Response rejected")
|
419
|
+
return soft ? false : validation_error("Duplicated Reference URI. SAML Response rejected")
|
420
|
+
end
|
421
|
+
|
422
|
+
verified_seis.push(sei)
|
423
|
+
end
|
424
|
+
end
|
425
|
+
|
426
|
+
signed_elements << signed_element
|
427
|
+
end
|
428
|
+
|
429
|
+
unless signature_nodes.length < 3 && !signed_elements.empty?
|
430
|
+
#return append_error("Found an unexpected number of Signature Element. SAML Response rejected")
|
431
|
+
return soft ? false : validation_error("Found an unexpected number of Signature Element. SAML Response rejected")
|
432
|
+
end
|
433
|
+
|
434
|
+
#if settings.security[:want_assertions_signed] && !(signed_elements.include? "Assertion")
|
435
|
+
if !(signed_elements.include? "Assertion")
|
436
|
+
#return append_error("The Assertion of the Response is not signed and the SP requires it")
|
437
|
+
return soft ? false : validation_error("L'asserzione non è firmata.")
|
438
|
+
end
|
439
|
+
|
440
|
+
true
|
441
|
+
end
|
442
|
+
|
443
|
+
def validate_structure(soft = true)
|
444
|
+
Dir.chdir(File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'schemas'))) do
|
445
|
+
@schema = Nokogiri::XML::Schema(IO.read('saml20protocol_schema.xsd'))
|
446
|
+
@xml = Nokogiri::XML(self.document.to_s)
|
447
|
+
end
|
448
|
+
if soft
|
449
|
+
@schema.validate(@xml).map{ return false }
|
450
|
+
else
|
451
|
+
@schema.validate(@xml).map{ |error| raise(Exception.new("#{error.message}\n\n#{@xml.to_s}")) }
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
def validate_response_state(soft = true)
|
456
|
+
if response.empty?
|
457
|
+
return soft ? false : validation_error("Blank response")
|
458
|
+
end
|
459
|
+
|
460
|
+
if settings.nil?
|
461
|
+
return soft ? false : validation_error("No settings on response")
|
462
|
+
end
|
463
|
+
|
464
|
+
if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil?
|
465
|
+
return soft ? false : validation_error("No fingerprint or certificate on settings")
|
466
|
+
end
|
467
|
+
|
468
|
+
true
|
469
|
+
end
|
470
|
+
|
471
|
+
# Validates the Destination, (If the SAML Response is received where expected).
|
472
|
+
# If the response was initialized with the :skip_destination option, this validation is skipped,
|
473
|
+
# If fails, the error is added to the errors array
|
474
|
+
# @return [Boolean] True if there is a Destination element that matches the Consumer Service URL, otherwise False
|
475
|
+
|
476
|
+
# @return [String|nil] Destination attribute from the SAML Response.
|
477
|
+
#
|
478
|
+
def destination
|
479
|
+
@destination ||= begin
|
480
|
+
node = REXML::XPath.first(
|
481
|
+
document,
|
482
|
+
"/p:Response",
|
483
|
+
{ "p" => PROTOCOL }
|
484
|
+
)
|
485
|
+
node.nil? ? nil : node.attributes['Destination']
|
486
|
+
end
|
487
|
+
end
|
488
|
+
|
489
|
+
def validate_destination(soft = true)
|
490
|
+
return (soft ? false : validation_error("La response non ha destination")) if destination.nil?
|
491
|
+
#return true if options[:skip_destination]
|
492
|
+
|
493
|
+
if destination.empty?
|
494
|
+
# error_msg = "The response has an empty Destination value"
|
495
|
+
# return append_error(error_msg)
|
496
|
+
return soft ? false : validation_error("The response has an empty Destination value")
|
497
|
+
end
|
498
|
+
|
499
|
+
return true if settings.assertion_consumer_service_url.nil? || settings.assertion_consumer_service_url.empty?
|
500
|
+
|
501
|
+
unless Ciam::Saml::Utils.uri_match?(destination, settings.assertion_consumer_service_url)
|
502
|
+
# error_msg = "The response was received at #{destination} instead of #{settings.assertion_consumer_service_url}"
|
503
|
+
# return append_error(error_msg)
|
504
|
+
return soft ? false : validation_error("The response was received at #{destination} instead of #{settings.assertion_consumer_service_url}")
|
505
|
+
end
|
506
|
+
|
507
|
+
true
|
508
|
+
end
|
509
|
+
|
510
|
+
def validate_assertion(soft = true)
|
511
|
+
#posso avere n nodi asserzione..forse
|
512
|
+
nodes_assertion = xpath_from_signed_assertion
|
513
|
+
unless nodes_assertion.blank?
|
514
|
+
#Elemento NameID non specificato
|
515
|
+
node_name_id = xpath_first_from_signed_assertion('/a:Subject/a:NameID')
|
516
|
+
unless node_name_id.blank?
|
517
|
+
return soft ? false : validation_error("Errore su Asserzione: NameID vuoto") if node_name_id.text.blank?
|
518
|
+
#controlli su attributo format
|
519
|
+
attr_format = node_name_id.attribute("Format")
|
520
|
+
return soft ? false : validation_error("Errore su Asserzione: Format su NameID vuoto") if attr_format.blank? || attr_format.to_s != "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" #45
|
521
|
+
#controlli su attributo NameQualifier
|
522
|
+
attr_name_qual = node_name_id.attribute("NameQualifier")
|
523
|
+
return soft ? false : validation_error("Errore su Asserzione: NameQualifier su NameID vuoto") if attr_name_qual.blank? || ( !attr_name_qual.blank? && attr_name_qual.value.blank?)#48 e 49
|
524
|
+
else
|
525
|
+
return soft ? false : validation_error("Errore su Asserzione: NameID non presente")
|
526
|
+
|
527
|
+
end
|
528
|
+
#Controlli su SubjectConfirmation
|
529
|
+
node_subj_conf = xpath_first_from_signed_assertion('/a:Subject/a:SubjectConfirmation')
|
530
|
+
unless node_subj_conf.blank?
|
531
|
+
#controlli su attributo Method
|
532
|
+
attr_method = node_subj_conf.attribute("Method")
|
533
|
+
return soft ? false : validation_error("Errore su Asserzione: Method su SubjectConfirmation vuoto") if attr_method.blank? || attr_method.to_s != "urn:oasis:names:tc:SAML:2.0:cm:bearer" #53 54 e 55
|
534
|
+
#Controlli su SubjectConfirmationData
|
535
|
+
node_subj_conf_data = xpath_first_from_signed_assertion('/a:Subject/a:SubjectConfirmation/a:SubjectConfirmationData')
|
536
|
+
unless node_subj_conf_data.blank?
|
537
|
+
#controllo attr Recipient, vuoto o diverso da AssertionConsumerServiceURL
|
538
|
+
attr_recipient = node_subj_conf_data.attribute("Recipient")
|
539
|
+
return soft ? false : validation_error("Errore su Asserzione: Recipient su SubjectConfirmationData vuoto o diverso da AssertionConsumerServiceURL") if attr_recipient.blank? || attr_recipient.to_s != settings.assertion_consumer_service_url #57 58 e 59
|
540
|
+
#controllo attr InResponseTo, vuoto o diverso da ID request
|
541
|
+
node_response = REXML::XPath.first(document, "/p:Response", { "p" => PROTOCOL})
|
542
|
+
id_request = node_response.attribute("InResponseTo")
|
543
|
+
attr_in_resp_to = node_subj_conf_data.attribute("InResponseTo")
|
544
|
+
return soft ? false : validation_error("Errore su Asserzione: InResponseTo su SubjectConfirmationData vuoto o diverso da ID request") if attr_in_resp_to.blank? || attr_in_resp_to.to_s != id_request.to_s #57 58 e 59
|
545
|
+
|
546
|
+
#controllo attr NotOnOrAfter se vuoto o non presente #63 64
|
547
|
+
attr_not_on_or_after = node_subj_conf_data.attribute("NotOnOrAfter")
|
548
|
+
return soft ? false : validation_error("Errore su Asserzione: NotOnOrAfter su SubjectConfirmationData mancante") if attr_not_on_or_after.blank?
|
549
|
+
|
550
|
+
|
551
|
+
else
|
552
|
+
return soft ? false : validation_error("Errore su Asserzione: SubjectConfirmationData non presente")
|
553
|
+
end
|
554
|
+
|
555
|
+
else
|
556
|
+
return soft ? false : validation_error("Errore su Asserzione: SubjectConfirmation non presente")
|
557
|
+
end
|
558
|
+
|
559
|
+
#Controlli su Conditions
|
560
|
+
node_conditions = xpath_first_from_signed_assertion('/a:Conditions')
|
561
|
+
unless node_conditions.blank?
|
562
|
+
attr_not_before = node_conditions.attribute("NotBefore")
|
563
|
+
return soft ? false : validation_error("Errore su Asserzione: Recipient su SubjectConfirmationData vuoto") if attr_not_before.blank? #75 76
|
564
|
+
#83 84. Assertion - Elemento AudienceRestriction di Condition mancante
|
565
|
+
node_conditions_audience_restrictions = xpath_first_from_signed_assertion('/a:Conditions/a:AudienceRestriction')
|
566
|
+
return soft ? false : validation_error("Errore su Asserzione: AudienceRestriction su Conditions vuoto") if node_conditions_audience_restrictions.blank? #83 84
|
567
|
+
#85 86 87. Assertion - Elemento Audience di AudienceRestriction mancante
|
568
|
+
node_conditions_audience_restrictions_audience = xpath_first_from_signed_assertion('/a:Conditions/a:AudienceRestriction/a:Audience')
|
569
|
+
#Ciamer.logger.error "\n\n node_conditions_audience_restrictions_audience #{node_conditions_audience_restrictions_audience}"
|
570
|
+
#Ciamer.logger.error "\n\n settings.issuer #{settings.issuer}"
|
571
|
+
return soft ? false : validation_error("Errore su Asserzione: Audience su AudienceRestriction vuoto") if node_conditions_audience_restrictions_audience.blank? || node_conditions_audience_restrictions_audience.text != settings.issuer #83 84
|
572
|
+
else
|
573
|
+
return soft ? false : validation_error("Errore su Asserzione: Conditions non presente")
|
574
|
+
end
|
575
|
+
|
576
|
+
|
577
|
+
node_auth_stat_context_class_ref = xpath_first_from_signed_assertion('/a:AuthnStatement/a:AuthnContext/a:AuthnContextClassRef')
|
578
|
+
|
579
|
+
node_attr_stmt_attribute_value = xpath_first_from_signed_assertion("/a:AttributeStatement/a:Attribute/a:AttributeValue")
|
580
|
+
#Elemento AttributeStatement presente, ma sottoelemento Attribute non specificato, caso 99
|
581
|
+
return soft ? false : validation_error("Errore su Asserzione: AttributeValue di Attribute su AttributeStatement vuoto") if node_attr_stmt_attribute_value.blank?
|
582
|
+
|
583
|
+
|
584
|
+
else
|
585
|
+
return soft ? false : validation_error("Errore su Asserzione: non presente")
|
586
|
+
end
|
587
|
+
true
|
588
|
+
end
|
589
|
+
|
590
|
+
|
591
|
+
def validate_name_format_attributes(soft=true)
|
592
|
+
unless attributes.blank?
|
593
|
+
return false if @attr_name_format.blank? || (@attr_name_format.length != (attributes.length / 2))
|
594
|
+
end
|
595
|
+
true
|
596
|
+
end
|
597
|
+
|
598
|
+
def get_fingerprint
|
599
|
+
idp_metadata = Ciam::Saml::Metadata.new(settings).get_idp_metadata
|
600
|
+
|
601
|
+
if settings.idp_cert
|
602
|
+
cert_text = Base64.decode64(settings.idp_cert)
|
603
|
+
cert = OpenSSL::X509::Certificate.new(cert_text)
|
604
|
+
Digest::SHA2.hexdigest(cert.to_der).upcase.scan(/../).join(":")
|
605
|
+
else
|
606
|
+
settings.idp_cert_fingerprint
|
607
|
+
end
|
608
|
+
|
609
|
+
end
|
610
|
+
|
611
|
+
def validate_conditions(soft = true)
|
612
|
+
return true if conditions.nil?
|
613
|
+
return true if options[:skip_conditions]
|
614
|
+
|
615
|
+
if not_before = parse_time(conditions, "NotBefore")
|
616
|
+
if Time.now.utc < not_before
|
617
|
+
return soft ? false : validation_error("Current time is earlier than NotBefore condition")
|
618
|
+
end
|
619
|
+
end
|
620
|
+
|
621
|
+
if not_on_or_after = parse_time(conditions, "NotOnOrAfter")
|
622
|
+
if Time.now.utc >= not_on_or_after
|
623
|
+
return soft ? false : validation_error("Current time is on or after NotOnOrAfter condition")
|
624
|
+
end
|
625
|
+
end
|
626
|
+
|
627
|
+
true
|
628
|
+
end
|
629
|
+
|
630
|
+
def parse_time(node, attribute)
|
631
|
+
if node && node.attributes[attribute]
|
632
|
+
Time.parse(node.attributes[attribute])
|
633
|
+
end
|
634
|
+
end
|
635
|
+
|
636
|
+
# Extracts all the appearances that matchs the subelt (pattern)
|
637
|
+
# Search on any Assertion that is signed, or has a Response parent signed
|
638
|
+
# @param subelt [String] The XPath pattern
|
639
|
+
# @return [Array of REXML::Element] Return all matches
|
640
|
+
#
|
641
|
+
def xpath_from_signed_assertion(subelt=nil)
|
642
|
+
doc = decrypted_document.nil? ? document : decrypted_document
|
643
|
+
node = REXML::XPath.match(
|
644
|
+
doc,
|
645
|
+
"/p:Response/a:Assertion[@ID=$id]#{subelt}",
|
646
|
+
{ "p" => PROTOCOL, "a" => ASSERTION },
|
647
|
+
{ 'id' => doc.signed_element_id }
|
648
|
+
)
|
649
|
+
node.concat( REXML::XPath.match(
|
650
|
+
doc,
|
651
|
+
"/p:Response[@ID=$id]/a:Assertion#{subelt}",
|
652
|
+
{ "p" => PROTOCOL, "a" => ASSERTION },
|
653
|
+
{ 'id' => doc.signed_element_id }
|
654
|
+
))
|
655
|
+
end
|
656
|
+
|
657
|
+
# Extracts the first appearance that matchs the subelt (pattern)
|
658
|
+
# Search on any Assertion that is signed, or has a Response parent signed
|
659
|
+
# @param subelt [String] The XPath pattern
|
660
|
+
# @return [REXML::Element | nil] If any matches, return the Element
|
661
|
+
#
|
662
|
+
def xpath_first_from_signed_assertion(subelt=nil)
|
663
|
+
doc = decrypted_document.nil? ? document : decrypted_document
|
664
|
+
node = REXML::XPath.first(
|
665
|
+
doc,
|
666
|
+
"/p:Response/a:Assertion[@ID=$id]#{subelt}",
|
667
|
+
{ "p" => PROTOCOL, "a" => ASSERTION },
|
668
|
+
{ 'id' => doc.signed_element_id }
|
669
|
+
)
|
670
|
+
node ||= REXML::XPath.first(
|
671
|
+
doc,
|
672
|
+
"/p:Response[@ID=$id]/a:Assertion#{subelt}",
|
673
|
+
{ "p" => PROTOCOL, "a" => ASSERTION },
|
674
|
+
{ 'id' => doc.signed_element_id }
|
675
|
+
)
|
676
|
+
node
|
677
|
+
end
|
678
|
+
|
679
|
+
|
680
|
+
end #chiudo classe
|
681
|
+
|
682
|
+
end
|
683
|
+
end
|