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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.document +5 -0
  3. data/Gemfile +4 -0
  4. data/README.md +127 -0
  5. data/ciam-es.gemspec +23 -0
  6. data/lib/ciam-es.rb +14 -0
  7. data/lib/ciam/ruby-saml/authrequest.rb +206 -0
  8. data/lib/ciam/ruby-saml/coding.rb +34 -0
  9. data/lib/ciam/ruby-saml/error_handling.rb +27 -0
  10. data/lib/ciam/ruby-saml/logging.rb +26 -0
  11. data/lib/ciam/ruby-saml/logout_request.rb +126 -0
  12. data/lib/ciam/ruby-saml/logout_response.rb +132 -0
  13. data/lib/ciam/ruby-saml/metadata.rb +509 -0
  14. data/lib/ciam/ruby-saml/request.rb +81 -0
  15. data/lib/ciam/ruby-saml/response.rb +683 -0
  16. data/lib/ciam/ruby-saml/settings.rb +89 -0
  17. data/lib/ciam/ruby-saml/utils.rb +225 -0
  18. data/lib/ciam/ruby-saml/validation_error.rb +7 -0
  19. data/lib/ciam/ruby-saml/version.rb +5 -0
  20. data/lib/ciam/xml_security.rb +166 -0
  21. data/lib/ciam/xml_security_new.rb +373 -0
  22. data/lib/schemas/saml20assertion_schema.xsd +283 -0
  23. data/lib/schemas/saml20protocol_schema.xsd +302 -0
  24. data/lib/schemas/xenc_schema.xsd +146 -0
  25. data/lib/schemas/xmldsig_schema.xsd +318 -0
  26. data/test/certificates/certificate1 +12 -0
  27. data/test/logoutrequest_test.rb +98 -0
  28. data/test/request_test.rb +53 -0
  29. data/test/response_test.rb +219 -0
  30. data/test/responses/adfs_response_sha1.xml +46 -0
  31. data/test/responses/adfs_response_sha256.xml +46 -0
  32. data/test/responses/adfs_response_sha384.xml +46 -0
  33. data/test/responses/adfs_response_sha512.xml +46 -0
  34. data/test/responses/no_signature_ns.xml +48 -0
  35. data/test/responses/open_saml_response.xml +56 -0
  36. data/test/responses/response1.xml.base64 +1 -0
  37. data/test/responses/response2.xml.base64 +79 -0
  38. data/test/responses/response3.xml.base64 +66 -0
  39. data/test/responses/response4.xml.base64 +93 -0
  40. data/test/responses/response5.xml.base64 +102 -0
  41. data/test/responses/response_with_ampersands.xml +139 -0
  42. data/test/responses/response_with_ampersands.xml.base64 +93 -0
  43. data/test/responses/simple_saml_php.xml +71 -0
  44. data/test/responses/wrapped_response_2.xml.base64 +150 -0
  45. data/test/settings_test.rb +43 -0
  46. data/test/test_helper.rb +65 -0
  47. data/test/xml_security_test.rb +123 -0
  48. metadata +145 -0
@@ -0,0 +1,26 @@
1
+ # Simplistic log class when we're running in Rails
2
+ module Ciam
3
+ module Saml
4
+ class Logging
5
+ def self.debug(message)
6
+ return if !!ENV["ruby-saml/testing"]
7
+
8
+ if defined? Rails
9
+ Rails.logger.debug message
10
+ else
11
+ puts message
12
+ end
13
+ end
14
+
15
+ def self.info(message)
16
+ return if !!ENV["ruby-saml/testing"]
17
+
18
+ if defined? Rails
19
+ Rails.logger.info message
20
+ else
21
+ puts message
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,126 @@
1
+ require 'uuid'
2
+
3
+ module Ciam::Saml
4
+ class LogoutRequest
5
+ ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
6
+ PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
7
+ DSIG = "http://www.w3.org/2000/09/xmldsig#"
8
+
9
+ include Coding
10
+ include Request
11
+ attr_reader :transaction_id
12
+ attr_accessor :settings
13
+
14
+ def initialize( options = {} )
15
+ opt = { :request => nil, :settings => nil }.merge(options)
16
+ @settings = opt[:settings]
17
+ @issue_instant = Ciam::Saml::LogoutRequest.timestamp
18
+ @request_params = Hash.new
19
+ # We need to generate a LogoutRequest to send to the IdP
20
+ if opt[:request].nil?
21
+ @transaction_id = UUID.new.generate
22
+ # The IdP sent us a LogoutRequest (IdP initiated SLO)
23
+ else
24
+ begin
25
+ @request = Ciam::XMLSecurity::SignedDocument.new( decode( opt[:request] ))
26
+ raise if @request.nil?
27
+ raise if @request.root.nil?
28
+ raise if @request.root.namespace != PROTOCOL
29
+ rescue
30
+ @request = Ciam::XMLSecurity::SignedDocument.new( inflate( decode( opt[:request] ) ) )
31
+ end
32
+ Logging.debug "LogoutRequest is: \n#{@request}"
33
+ end
34
+ end
35
+
36
+ def create( options = {} )
37
+ opt = { :name_id => nil, :session_index => nil, :extra_parameters => nil }.merge(options)
38
+ return nil unless opt[:name_id]
39
+
40
+ @request = REXML::Document.new
41
+ @request.context[:attribute_quote] = :quote
42
+
43
+
44
+ root = @request.add_element "saml2p:LogoutRequest", { "xmlns:saml2p" => PROTOCOL }
45
+ root.attributes['ID'] = @transaction_id
46
+ root.attributes['IssueInstant'] = @issue_instant
47
+ root.attributes['Version'] = "2.0"
48
+ root.attributes['Destination'] = @settings.single_logout_destination
49
+
50
+ issuer = root.add_element "saml2:Issuer", { "xmlns:saml2" => ASSERTION }
51
+ issuer.attributes['Format'] = "urn:oasis:names:tc:SAML:2.0:nameid-format:entity"
52
+ #issuer.text = @settings.issuer
53
+ #per la federazione trentina qui ci vanno i metadati...
54
+ issuer.text = @settings.idp_metadata
55
+
56
+ name_id = root.add_element "saml2:NameID", { "xmlns:saml2" => ASSERTION }
57
+ name_id.attributes['Format'] = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
58
+ name_id.attributes['NameQualifier'] = @settings.idp_name_qualifier
59
+ name_id.text = opt[:name_id]
60
+ # I believe the rest of these are optional
61
+ if @settings && @settings.sp_name_qualifier
62
+ name_id.attributes["SPNameQualifier"] = @settings.sp_name_qualifier
63
+ end
64
+ if opt[:session_index]
65
+ session_index = root.add_element "saml2p:SessionIndex" #, { "xmlns:samlp" => PROTOCOL }
66
+ session_index.text = opt[:session_index]
67
+ end
68
+ Logging.debug "Created LogoutRequest: #{@request}"
69
+ meta = Metadata.new(@settings)
70
+ return meta.create_slo_request( to_s, opt[:extra_parameters] )
71
+ #action, content = binding_select("SingleLogoutService")
72
+ #Logging.debug "action: #{action} content: #{content}"
73
+ #return [action, content]
74
+ end
75
+
76
+ # function to return the created request as an XML document
77
+ def to_xml
78
+ text = ""
79
+ @request.write(text, 1)
80
+ return text
81
+ end
82
+ def to_s
83
+ @request.to_s
84
+ end
85
+ # Functions for pulling values out from an IdP initiated LogoutRequest
86
+ def name_id
87
+ element = REXML::XPath.first(@request, "/p:LogoutRequest/a:NameID", {
88
+ "p" => PROTOCOL, "a" => ASSERTION } )
89
+ return nil if element.nil?
90
+ # Can't seem to get this to work right...
91
+ #element.context[:compress_whitespace] = ["NameID"]
92
+ #element.context[:compress_whitespace] = :all
93
+ str = element.text.gsub(/^\s+/, "")
94
+ str.gsub!(/\s+$/, "")
95
+ return str
96
+ end
97
+
98
+ def transaction_id
99
+ return @transaction_id if @transaction_id
100
+ element = REXML::XPath.first(@request, "/p:LogoutRequest", {
101
+ "p" => PROTOCOL} )
102
+ return nil if element.nil?
103
+ return element.attributes["ID"]
104
+ end
105
+ def is_valid?
106
+ validate(soft = true)
107
+ end
108
+
109
+ def validate!
110
+ validate( soft = false )
111
+ end
112
+ def validate( soft = true )
113
+ return false if @request.nil?
114
+ return false if @request.validate(@settings, soft) == false
115
+
116
+ return true
117
+
118
+ end
119
+ private
120
+
121
+ def self.timestamp
122
+ Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
123
+ end
124
+
125
+ end
126
+ end
@@ -0,0 +1,132 @@
1
+ #encoding: utf-8
2
+
3
+ require "rexml/document"
4
+
5
+ module Ciam
6
+ module Saml
7
+ class LogoutResponse
8
+ include Coding
9
+ include Request
10
+ ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
11
+ PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
12
+ DSIG = "http://www.w3.org/2000/09/xmldsig#"
13
+
14
+ def initialize( options = { } )
15
+ opt = { :response => nil, :settings => nil }.merge(options)
16
+ # We've recieved a LogoutResponse from the IdP
17
+ if opt[:response]
18
+ begin
19
+ @response = Ciam::XMLSecurity::SignedDocument.new(decode( opt[:response] ))
20
+ # Check to see if we have a root tag using the "protocol" namespace.
21
+ # If not, it means this is deflated text and we need to raise to
22
+ # the rescue below
23
+ raise if @response.nil?
24
+ raise if @response.root.nil?
25
+ raise if @response.root.namespace != PROTOCOL
26
+ document
27
+ rescue
28
+ @response = Ciam::XMLSecurity::SignedDocument.new( inflate(decode( opt[:response] ) ) )
29
+ end
30
+ end
31
+ # We plan to create() a new LogoutResponse
32
+ if opt[:settings]
33
+ @settings = opt[:settings]
34
+ end
35
+ end
36
+
37
+ # Create a LogoutResponse to to the IdP's LogoutRequest
38
+ # (For IdP initiated SLO)
39
+ def create( options )
40
+ opt = { :transaction_id => nil,
41
+ :in_response_to => nil,
42
+ :status => "urn:oasis:names:tc:SAML:2.0:status:Success",
43
+ :extra_parameters => nil }.merge(options)
44
+ return nil if opt[:transaction_id].nil?
45
+ @response = REXML::Document.new
46
+ @response.context[:attribute_quote] = :quote
47
+ uuid = "_" + UUID.new.generate
48
+ time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
49
+ root = @response.add_element "saml2p:LogoutResponse", { "xmlns:saml2p" => PROTOCOL }
50
+ root.attributes['ID'] = uuid
51
+ root.attributes['IssueInstant'] = time
52
+ root.attributes['Version'] = "2.0"
53
+ # Just convenient naming to accept both names as InResponseTo
54
+ if opt[:transaction_id]
55
+ root.attributes['InResponseTo'] = opt[:transaction_id]
56
+ elsif opt[:in_response_to]
57
+ root.attributes['InResponseTo'] = opt[:in_response_to]
58
+ end
59
+ if opt[:status]
60
+ status = root.add_element "saml2p:Status"
61
+ status_code = status.add_element "saml2p:StatusCode", {
62
+ "Value" => opt[:status]
63
+ }
64
+ end
65
+ if @settings && @settings.issuer
66
+ issuer = root.add_element "saml:Issuer", {
67
+ "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion"
68
+ }
69
+ issuer.text = @settings.issuer
70
+ end
71
+ meta = Metadata.new( @settings )
72
+ Logging.debug "Created LogoutResponse:\n#{@response}"
73
+ return meta.create_slo_response( to_s, opt[:extra_parameters] )
74
+
75
+ #root.attributes['Destination'] = action
76
+
77
+ end
78
+ # function to return the created request as an XML document
79
+ def to_xml
80
+ text = ""
81
+ @response.write(text, 1)
82
+ return text
83
+ end
84
+ def to_s
85
+ @response.to_s
86
+ end
87
+
88
+ def issuer
89
+ element = REXML::XPath.first(@response, "/p:LogoutResponse/a:Issuer", {
90
+ "p" => PROTOCOL, "a" => ASSERTION} )
91
+ return nil if element.nil?
92
+ element.text
93
+ end
94
+
95
+ def in_response_to
96
+ element = REXML::XPath.first(@response, "/p:LogoutResponse", {
97
+ "p" => PROTOCOL })
98
+ return nil if element.nil?
99
+ element.attributes["InResponseTo"]
100
+ end
101
+
102
+ def success?
103
+ element = REXML::XPath.first(@response, "/p:LogoutResponse/p:Status/p:StatusCode", {
104
+ "p" => PROTOCOL })
105
+ return false if element.nil?
106
+ element.attributes["Value"] == "urn:oasis:names:tc:SAML:2.0:status:Success"
107
+
108
+ end
109
+ def is_valid?
110
+ validate(soft = true)
111
+ end
112
+
113
+ def validate!
114
+ validate( soft = false )
115
+ end
116
+ def validate( soft = true )
117
+ return false if @response.nil?
118
+ # Skip validation with a failed response if we don't have settings
119
+ return false if @settings.nil?
120
+ return false if @response.validate(@settings, soft) == false
121
+
122
+ return true
123
+
124
+ end
125
+
126
+ protected
127
+ def document
128
+ REXML::Document.new(@response)
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,509 @@
1
+ require "rexml/document"
2
+ require "rexml/xpath"
3
+ require "net/https"
4
+ require "uri"
5
+ require "digest/md5"
6
+ require "nokogiri"
7
+ require_relative "../xml_security_new" #fa il require della nokogiri
8
+
9
+ # Class to return SP metadata based on the settings requested.
10
+ # Return this XML in a controller, then give that URL to the the
11
+ # IdP administrator. The IdP will poll the URL and your settings
12
+ # will be updated automatically
13
+ module Ciam
14
+ module Saml
15
+ class Metadata
16
+ include REXML
17
+ include Coding
18
+ # a few symbols for SAML class names
19
+ HTTP_POST = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
20
+ HTTP_GET = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
21
+
22
+ attr_accessor :uuid
23
+
24
+ def initialize(settings=nil)
25
+ if settings
26
+ @settings = settings
27
+ end
28
+ end
29
+
30
+ def generate(settings)
31
+ #meta_doc = REXML::Document.new
32
+ meta_doc = Ciam::XMLSecurityNew::Document.new
33
+ if settings.aggregato
34
+ root = meta_doc.add_element "md:EntityDescriptor", {
35
+ "xmlns:md" => "urn:oasis:names:tc:SAML:2.0:metadata",
36
+ "xmlns:xml" => "http://www.w3.org/XML/1998/namespace",
37
+ "xmlns:ciam" => "https://ciam.gov.it/saml-extensions",
38
+ }
39
+ else
40
+ root = meta_doc.add_element "md:EntityDescriptor", {
41
+ "xmlns:md" => "urn:oasis:names:tc:SAML:2.0:metadata",
42
+ "xmlns:xml" => "http://www.w3.org/XML/1998/namespace"
43
+ }
44
+ end
45
+
46
+ if settings.issuer != nil
47
+ root.attributes["entityID"] = settings.issuer
48
+ end
49
+ #Tolto per non far cambiare sempre il metadata
50
+ #uuid = "_" + UUID.new.generate
51
+ #genero l'id come hash dell'entityID
52
+ uuid = "_"+Digest::MD5.hexdigest(settings.issuer)
53
+ self.uuid = uuid
54
+ root.attributes["ID"] = uuid
55
+
56
+ sp_sso = root.add_element "md:SPSSODescriptor", {
57
+ "protocolSupportEnumeration" => "urn:oasis:names:tc:SAML:2.0:protocol",
58
+ "WantAssertionsSigned" => "true",
59
+ "AuthnRequestsSigned" => "true"
60
+
61
+ }
62
+
63
+
64
+ # if settings.sp_cert != nil
65
+ # keyDescriptor = sp_sso.add_element "md:KeyDescriptor", {
66
+ # "use" => "signing"
67
+ # }
68
+ # keyInfo = keyDescriptor.add_element "ds:KeyInfo", {
69
+ # "xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"
70
+ # }
71
+ # x509Data = keyInfo.add_element "ds:X509Data"
72
+ # x509Certificate = x509Data.add_element "ds:X509Certificate"
73
+ # file = ""
74
+ # File.foreach(settings.sp_cert){ |line|
75
+ # file += line unless (line.include?("RSA PUBLIC KEY") || line.include?("CERTIFICATE"))
76
+ # }
77
+ # x509Certificate.text = file
78
+ # end
79
+
80
+ # Add KeyDescriptor if messages will be signed / encrypted
81
+ #cert = settings.get_sp_cert
82
+ cert = settings.get_cert(settings.sp_cert)
83
+ if cert
84
+
85
+ if cert.is_a?(String)
86
+ cert = OpenSSL::X509::Certificate.new(cert)
87
+ end
88
+
89
+ cert_text = Base64.encode64(cert.to_der).to_s.gsub(/\n/, "").gsub(/\t/, "")
90
+ kd = sp_sso.add_element "md:KeyDescriptor", { "use" => "signing" }
91
+ ki = kd.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"}
92
+ xd = ki.add_element "ds:X509Data"
93
+ xc = xd.add_element "ds:X509Certificate"
94
+ xc.text = cert_text
95
+
96
+ # kd2 = sp_sso.add_element "md:KeyDescriptor", { "use" => "encryption" }
97
+ # ki2 = kd2.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"}
98
+ # xd2 = ki2.add_element "ds:X509Data"
99
+ # xc2 = xd2.add_element "ds:X509Certificate"
100
+ # xc2.text = cert_text
101
+ end
102
+
103
+ if !settings.sp_external_consumer_cert.nil? && settings.sp_external_consumer_cert.length > 0
104
+ settings.sp_external_consumer_cert.each{ |cert_cons_external|
105
+ cert_ex = settings.get_cert(cert_cons_external)
106
+ if cert_ex
107
+
108
+ if cert_ex.is_a?(String)
109
+ cert_ex = OpenSSL::X509::Certificate.new(cert_ex)
110
+ end
111
+
112
+ cert_text = Base64.encode64(cert_ex.to_der).to_s.gsub(/\n/, "").gsub(/\t/, "")
113
+ kd = sp_sso.add_element "md:KeyDescriptor", { "use" => "signing" }
114
+ ki = kd.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"}
115
+ xd = ki.add_element "ds:X509Data"
116
+ xc = xd.add_element "ds:X509Certificate"
117
+ xc.text = cert_text
118
+ end
119
+ }
120
+ end
121
+
122
+ if settings.single_logout_service_url != nil
123
+ sp_sso.add_element "md:SingleLogoutService", {
124
+ "Binding" => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
125
+ "Location" => settings.single_logout_service_url
126
+ }
127
+ sp_sso.add_element "md:SingleLogoutService", {
128
+ "Binding" => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
129
+ "Location" => settings.single_logout_service_url
130
+ }
131
+ end
132
+
133
+ #Logout dei servizi esterni
134
+ unless settings.hash_assertion_consumer.blank?
135
+ settings.hash_assertion_consumer.each_pair{ |index, hash_service|
136
+ unless hash_service['logout'].blank?
137
+ sp_sso.add_element "md:SingleLogoutService", {
138
+ "Binding" => hash_service['logout']['binding'] || "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
139
+ "Location" => hash_service['logout']['location']
140
+ }
141
+ end
142
+ }
143
+ end
144
+
145
+ name_identifier_formats = settings.name_identifier_format
146
+ if name_identifier_formats != nil
147
+ name_id = []
148
+ name_identifier_formats.each_with_index{ |format, index|
149
+ name_id[index] = sp_sso.add_element "md:NameIDFormat"
150
+ name_id[index].text = format
151
+ }
152
+
153
+ end
154
+
155
+ if settings.assertion_consumer_service_url
156
+
157
+ #ciclo e creo i vari tag AssertionConsumerService
158
+ settings.hash_assertion_consumer.each_pair{ |index, hash_service|
159
+
160
+ sp_sso.add_element "md:AssertionConsumerService", {
161
+ "Binding" => settings.assertion_consumer_service_binding,
162
+ "Location" => (hash_service['external'] ? hash_service['url_consumer'] : settings.assertion_consumer_service_url ),
163
+ "isDefault" => hash_service['default'],
164
+ "index" => index
165
+ }
166
+ }
167
+
168
+ # #Caso con eidas
169
+ # sp_sso.add_element "md:AssertionConsumerService", {
170
+ # "Binding" => settings.assertion_consumer_service_binding,
171
+ # "Location" => settings.assertion_consumer_service_url,
172
+ # "index" => 99
173
+ # }
174
+
175
+ # sp_sso.add_element "md:AssertionConsumerService", {
176
+ # "Binding" => settings.assertion_consumer_service_binding,
177
+ # "Location" => settings.assertion_consumer_service_url,
178
+ # "index" => 100
179
+ # }
180
+
181
+ settings.hash_assertion_consumer.each_pair{ |index, hash_service|
182
+
183
+ #AttributeConsumingService
184
+ attr_cons_service = sp_sso.add_element "md:AttributeConsumingService", {
185
+ "index" => index,
186
+ }
187
+ service_name = attr_cons_service.add_element "md:ServiceName", {
188
+ "xml:lang" => "it"
189
+ }
190
+ service_name.text = hash_service['testo']
191
+ unless hash_service['description'].blank?
192
+ service_description = attr_cons_service.add_element "md:ServiceDescription", {
193
+ "xml:lang" => "it"
194
+ }
195
+ service_description.text = hash_service['description']
196
+ end
197
+
198
+ if hash_service['array_campi'].is_a?(Array)
199
+ hash_service['array_campi'].each_with_index{ |attribute, index|
200
+ attr_cons_service.add_element "md:RequestedAttribute", {
201
+ "Name" => attribute
202
+ }
203
+ }
204
+ else #hash
205
+ hash_service['array_campi'].each_pair{ |attribute, name_format|
206
+ attr_cons_service.add_element "md:RequestedAttribute", {
207
+ "Name" => attribute,
208
+ "NameFormat" => name_format
209
+ }
210
+ }
211
+ end
212
+ }
213
+
214
+
215
+ end
216
+ #organization
217
+ organization = root.add_element "md:Organization"
218
+ org_name = organization.add_element "md:OrganizationName", {
219
+ "xml:lang" => "it"
220
+ }
221
+ org_name.text = settings.organization['org_name']
222
+ org_display_name = organization.add_element "md:OrganizationDisplayName", {
223
+ "xml:lang" => "it"
224
+ }
225
+
226
+ org_display_name.text = settings.organization['org_display_name']+(settings.aggregato ? " tramite #{settings.hash_aggregatore['soggetto_aggregatore']}" : '')
227
+ org_url = organization.add_element "md:OrganizationURL", {
228
+ "xml:lang" => "it"
229
+ }
230
+ org_url.text = settings.organization['org_url']
231
+
232
+ #ContactPerson per sp aggregato
233
+ if settings.aggregato
234
+ contact_person_aggregatore = root.add_element "md:ContactPerson", {
235
+ "contactType" => "other",
236
+ "ciam:entityType" => "ciam:aggregator"
237
+ }
238
+ company = contact_person_aggregatore.add_element "md:Company"
239
+ company.text = settings.hash_aggregatore['soggetto_aggregatore']
240
+
241
+ extensions_aggregatore = contact_person_aggregatore.add_element "md:Extensions"
242
+ vat_number_aggregatore = extensions_aggregatore.add_element "ciam:VATNumber"
243
+ vat_number_aggregatore.text = settings.hash_aggregatore['piva_aggregatore']
244
+
245
+ ipa_code_aggregatore = extensions_aggregatore.add_element "ciam:IPACode"
246
+ ipa_code_aggregatore.text = settings.hash_aggregatore['cipa_aggregatore']
247
+
248
+ fiscal_code_aggregatore = extensions_aggregatore.add_element "ciam:FiscalCode"
249
+ fiscal_code_aggregatore.text = settings.hash_aggregatore['cf_aggregatore']
250
+
251
+ contact_person_aggregato = root.add_element "md:ContactPerson", {
252
+ "contactType" => "other",
253
+ "ciam:entityType" => "ciam:aggregated"
254
+ }
255
+ company = contact_person_aggregato.add_element "md:Company"
256
+ company.text = settings.organization['org_name']
257
+
258
+ extensions_aggregato = contact_person_aggregato.add_element "md:Extensions"
259
+ unless settings.hash_aggregatore['soggetto_aggregato']['vat_number'].blank?
260
+ vat_number_aggregato = extensions_aggregato.add_element "ciam:VATNumber"
261
+ vat_number_aggregato.text = settings.hash_aggregatore['soggetto_aggregato']['vat_number']
262
+ end
263
+ unless settings.hash_aggregatore['soggetto_aggregato']['ipa_code'].blank?
264
+ ipa_code_aggregato = extensions_aggregato.add_element "ciam:IPACode"
265
+ ipa_code_aggregato.text = settings.hash_aggregatore['soggetto_aggregato']['ipa_code']
266
+ end
267
+ unless settings.hash_aggregatore['soggetto_aggregato']['fiscal_code'].blank?
268
+ fiscal_code_aggregato = extensions_aggregato.add_element "ciam:FiscalCode"
269
+ fiscal_code_aggregato.text = settings.hash_aggregatore['soggetto_aggregato']['fiscal_code']
270
+ end
271
+ end
272
+
273
+ #meta_doc << REXML::XMLDecl.new(version='1.0', encoding='UTF-8')
274
+ meta_doc << REXML::XMLDecl.new("1.0", "UTF-8")
275
+
276
+
277
+ #SE SERVE ANCHE ENCRYPTION
278
+ # # Add KeyDescriptor if messages will be signed / encrypted
279
+ #
280
+ # if cert
281
+ # cert_text = Base64.encode64(cert.to_der).gsub("\n", '')
282
+ # kd = sp_sso.add_element "md:KeyDescriptor", { "use" => "signing" }
283
+ # ki = kd.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"}
284
+ # xd = ki.add_element "ds:X509Data"
285
+ # xc = xd.add_element "ds:X509Certificate"
286
+ # xc.text = cert_text
287
+
288
+ # kd2 = sp_sso.add_element "md:KeyDescriptor", { "use" => "encryption" }
289
+ # ki2 = kd2.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"}
290
+ # xd2 = ki2.add_element "ds:X509Data"
291
+ # xc2 = xd2.add_element "ds:X509Certificate"
292
+ # xc2.text = cert_text
293
+ # end
294
+
295
+ #cert = settings.get_sp_cert
296
+ cert = settings.get_cert(settings.sp_cert) #inserisco il certificato principale
297
+ # embed signature
298
+ if settings.metadata_signed && settings.sp_private_key && settings.sp_cert
299
+ private_key = settings.get_sp_key
300
+ meta_doc.sign_document(private_key, cert)
301
+ end
302
+ ret = ""
303
+ # stampo come stringa semplice i metadata per non avere problemi con validazione firma
304
+ ret = meta_doc.to_s
305
+ #Logging.debug "Generated metadata:\n#{ret}"
306
+
307
+ return ret
308
+
309
+ end
310
+
311
+ def create_sso_request(message, extra_parameters = {} )
312
+ build_message( :type => "SAMLRequest",
313
+ :service => "SingleSignOnService",
314
+ :message => message, :extra_parameters => extra_parameters)
315
+ end
316
+ def create_sso_response(message, extra_parameters = {} )
317
+ build_message( :type => "SAMLResponse",
318
+ :service => "SingleSignOnService",
319
+ :message => message, :extra_parameters => extra_parameters)
320
+ end
321
+ def create_slo_request(message, extra_parameters = {} )
322
+ build_message( :type => "SAMLRequest",
323
+ :service => "SingleLogoutService",
324
+ :message => message, :extra_parameters => extra_parameters)
325
+ end
326
+ def create_slo_response(message, extra_parameters = {} )
327
+ build_message( :type => "SAMLResponse",
328
+ :service => "SingleLogoutService",
329
+ :message => message, :extra_parameters => extra_parameters)
330
+ end
331
+
332
+ # Construct a SAML message using information in the IdP metadata.
333
+ # :type can be either "SAMLRequest" or "SAMLResponse"
334
+ # :service refers to the Binding method,
335
+ # either "SingleLogoutService" or "SingleSignOnService"
336
+ # :message is the SAML message itself (XML)
337
+ # I've provided easy to use wrapper functions above
338
+ def build_message( options = {} )
339
+ opt = { :type => nil, :service => nil, :message => nil, :extra_parameters => nil }.merge(options)
340
+ url = binding_select( opt[:service] )
341
+ return message_get( opt[:type], url, opt[:message], opt[:extra_parameters] )
342
+ end
343
+
344
+ # get the IdP metadata, and select the appropriate SSO binding
345
+ # that we can support. Currently this is HTTP-Redirect and HTTP-POST
346
+ # but more could be added in the future
347
+ def binding_select(service)
348
+ # first check if we're still using the old hard coded method for
349
+ # backwards compatability
350
+ if service == "SingleSignOnService" && @settings.idp_sso_target_url != nil
351
+ return @settings.idp_sso_target_url
352
+ end
353
+ if service == "SingleLogoutService" && @settings.idp_slo_target_url != nil
354
+ return @settings.idp_slo_target_url
355
+ end
356
+
357
+ meta_doc = get_idp_metadata
358
+
359
+ return nil unless meta_doc
360
+ # first try GET (REDIRECT)
361
+ sso_element = REXML::XPath.first(meta_doc, "/EntityDescriptor/IDPSSODescriptor/#{service}[@Binding='#{HTTP_GET}']")
362
+ if !sso_element.nil?
363
+ @URL = sso_element.attributes["Location"]
364
+ Logging.debug "binding_select: GET from #{@URL}"
365
+ return @URL
366
+ end
367
+
368
+ # next try post
369
+ sso_element = REXML::XPath.first(meta_doc, "/EntityDescriptor/IDPSSODescriptor/#{service}[@Binding='#{HTTP_POST}']")
370
+ if !sso_element.nil?
371
+ @URL = sso_element.attributes["Location"]
372
+ #Logging.debug "binding_select: POST to #{@URL}"
373
+ return @URL
374
+ end
375
+
376
+ # other types we might want to add in the future: SOAP, Artifact
377
+ end
378
+
379
+
380
+ def fetch(uri_str, limit = 10)
381
+ # You should choose a better exception.
382
+ raise ArgumentError, 'too many HTTP redirects' if limit == 0
383
+
384
+ uri = URI.parse(uri_str)
385
+ if uri.scheme == "http"
386
+ response = Net::HTTP.get_response(uri)
387
+ elsif uri.scheme == "https"
388
+ http = Net::HTTP.new(uri.host, uri.port)
389
+ http.use_ssl = true
390
+ # Most IdPs will probably use self signed certs
391
+ #http.verify_mode = OpenSSL::SSL::VERIFY_PEER
392
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
393
+ get = Net::HTTP::Get.new(uri.request_uri)
394
+ response = http.request(get)
395
+ end
396
+
397
+ case response
398
+ when Net::HTTPSuccess then
399
+ response
400
+ when Net::HTTPRedirection then
401
+ location = response['location']
402
+ warn "redirected to #{location}"
403
+ fetch(location, limit - 1)
404
+ else
405
+ response.value
406
+ end
407
+ end
408
+
409
+
410
+
411
+ # Retrieve the remote IdP metadata from the URL or a cached copy
412
+ # returns a REXML document of the metadata
413
+ def get_idp_metadata
414
+ return false if @settings.idp_metadata.nil?
415
+
416
+ # Look up the metdata in cache first
417
+ id = Digest::MD5.hexdigest(@settings.idp_metadata)
418
+ response = fetch(@settings.idp_metadata)
419
+ #meta_text = response.body
420
+ #testo_response = meta_text.sub!(' xmlns:xml="http://www.w3.org/XML/1998/namespace"', '') da errori
421
+ #uso nokogiri per cercare il certificato, uso la funzione che rimuove tutti i namespace
422
+ doc_noko = Nokogiri::XML(response.body.gsub(/\n/, "").gsub(/\t/, "")) #modifica per poste
423
+ doc_noko.remove_namespaces!
424
+ extract_certificate(doc_noko)
425
+ doc_rexml = REXML::Document.new(doc_noko.to_xml)
426
+
427
+ return doc_rexml
428
+
429
+ # USE OF CACHE WITH CERTIFICATE
430
+ # lookup = @cache.read(id)
431
+ # if lookup != nil
432
+ # Logging.debug "IdP metadata cached lookup for #{@settings.idp_metadata}"
433
+ # doc = REXML::Document.new( lookup )
434
+ # extract_certificate( doc )
435
+ # return doc
436
+ # end
437
+
438
+ # Logging.debug "IdP metadata cache miss on #{@settings.idp_metadata}"
439
+ # # cache miss
440
+ # if File.exists?(@settings.idp_metadata)
441
+ # fp = File.open( @settings.idp_metadata, "r")
442
+ # meta_text = fp.read
443
+ # else
444
+ # uri = URI.parse(@settings.idp_metadata)
445
+ # if uri.scheme == "http"
446
+ # response = Net::HTTP.get_response(uri)
447
+ # meta_text = response.body
448
+ # elsif uri.scheme == "https"
449
+ # http = Net::HTTP.new(uri.host, uri.port)
450
+ # http.use_ssl = true
451
+ # # Most IdPs will probably use self signed certs
452
+ # #http.verify_mode = OpenSSL::SSL::VERIFY_PEER
453
+ # http.verify_mode = OpenSSL::SSL::VERIFY_NONE
454
+ # get = Net::HTTP::Get.new(uri.request_uri)
455
+ # response = http.request(get)
456
+ # meta_text = response.body
457
+ # end
458
+ # end
459
+ # # Add it to the cache
460
+ # @cache.write(id, meta_text, @settings.idp_metadata_ttl )
461
+ # doc = REXML::Document.new( meta_text )
462
+ # extract_certificate(doc)
463
+ # return doc
464
+ end
465
+
466
+ def extract_certificate(meta_doc)
467
+ #ricerco il certificato con nokogiri
468
+ # pull out the x509 tag
469
+ x509 = meta_doc.xpath("//EntityDescriptor//IDPSSODescriptor//KeyDescriptor//KeyInfo//X509Data//X509Certificate")
470
+
471
+ #x509 = REXML::XPath.first(meta_doc, "/md:EntityDescriptor/md:IDPSSODescriptor"+"/md:KeyDescriptor"+"/ds:KeyInfo/ds:X509Data/ds:X509Certificate")
472
+ # If the IdP didn't specify the use attribute
473
+ if x509.nil?
474
+ x509 = meta_doc.xpath("//EntityDescriptor//IDPSSODescriptor//KeyDescriptor//KeyInfo//X509Data//X509Certificate")
475
+ # x509 = REXML::XPath.first(meta_doc,
476
+ # "/EntityDescriptor/IDPSSODescriptor" +
477
+ # "/KeyDescriptor" +
478
+ # "/ds:KeyInfo/ds:X509Data/ds:X509Certificate"
479
+ # )
480
+ end
481
+ @settings.idp_cert = x509.children.to_s.gsub(/\n/, "").gsub(/\t/, "")
482
+ end
483
+
484
+ # construct the parameter list on the URL and return
485
+ def message_get( type, url, message, extra_parameters = {} )
486
+ params = Hash.new
487
+ if extra_parameters
488
+ params.merge!(extra_parameters)
489
+ end
490
+ # compress GET requests to try and stay under that 8KB request limit
491
+ #deflate of samlrequest
492
+ params[type] = encode( deflate( message ) )
493
+ #Logging.debug "#{type}=#{params[type]}"
494
+
495
+ uri = Addressable::URI.parse(url)
496
+ if uri.query_values == nil
497
+ uri.query_values = params
498
+ else
499
+ # solution to stevenwilkin's parameter merge
500
+ uri.query_values = params.merge(uri.query_values)
501
+ end
502
+ url = uri.to_s
503
+ #Logging.debug "Sending to URL #{url}"
504
+ return url
505
+ end
506
+
507
+ end
508
+ end
509
+ end