spid-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 +15 -0
  2. data/.document +5 -0
  3. data/.travis.yml +5 -0
  4. data/Gemfile +12 -0
  5. data/LICENSE +19 -0
  6. data/README.md +124 -0
  7. data/Rakefile +41 -0
  8. data/lib/schemas/saml20assertion_schema.xsd +283 -0
  9. data/lib/schemas/saml20protocol_schema.xsd +302 -0
  10. data/lib/schemas/xenc_schema.xsd +146 -0
  11. data/lib/schemas/xmldsig_schema.xsd +318 -0
  12. data/lib/spid/ruby-saml/authrequest.rb +196 -0
  13. data/lib/spid/ruby-saml/coding.rb +34 -0
  14. data/lib/spid/ruby-saml/logging.rb +26 -0
  15. data/lib/spid/ruby-saml/logout_request.rb +126 -0
  16. data/lib/spid/ruby-saml/logout_response.rb +132 -0
  17. data/lib/spid/ruby-saml/metadata.rb +353 -0
  18. data/lib/spid/ruby-saml/request.rb +81 -0
  19. data/lib/spid/ruby-saml/response.rb +202 -0
  20. data/lib/spid/ruby-saml/settings.rb +72 -0
  21. data/lib/spid/ruby-saml/validation_error.rb +7 -0
  22. data/lib/spid/ruby-saml/version.rb +5 -0
  23. data/lib/spid-es.rb +14 -0
  24. data/lib/xml_security.rb +165 -0
  25. data/spid-es.gemspec +23 -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 +158 -0
@@ -0,0 +1,126 @@
1
+ require 'uuid'
2
+
3
+ module Spid::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 = Spid::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 = 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 = 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 Spid
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 = 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 = 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,353 @@
1
+ require "rexml/document"
2
+ require "rexml/xpath"
3
+ require "net/https"
4
+ require "uri"
5
+ require "digest/md5"
6
+ require "nokogiri"
7
+ require "xml_security_new" #fa il require della nokogiri
8
+
9
+
10
+ # Class to return SP metadata based on the settings requested.
11
+ # Return this XML in a controller, then give that URL to the the
12
+ # IdP administrator. The IdP will poll the URL and your settings
13
+ # will be updated automatically
14
+ module Spid
15
+ module Saml
16
+ class Metadata
17
+ include REXML
18
+ include Coding
19
+ # a few symbols for SAML class names
20
+ HTTP_POST = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
21
+ HTTP_GET = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
22
+
23
+ def initialize(settings=nil)
24
+ if settings
25
+ @settings = settings
26
+ end
27
+ end
28
+
29
+ def generate(settings)
30
+ #meta_doc = REXML::Document.new
31
+ meta_doc = ::XMLSecurityNew::Document.new
32
+ root = meta_doc.add_element "md:EntityDescriptor", {
33
+ "xmlns:md" => "urn:oasis:names:tc:SAML:2.0:metadata",
34
+ "xmlns:xml" => "http://www.w3.org/XML/1998/namespace",
35
+ "cacheDuration" => "P1M"
36
+ }
37
+ if settings.issuer != nil
38
+ root.attributes["entityID"] = settings.issuer
39
+ end
40
+ sp_sso = root.add_element "md:SPSSODescriptor", {
41
+ "protocolSupportEnumeration" => "urn:oasis:names:tc:SAML:2.0:protocol",
42
+ #"WantAssertionsSigned" => "true",
43
+ "AuthnRequestSigned" => "true"
44
+
45
+ }
46
+ name_identifier_formats = settings.name_identifier_format
47
+ if name_identifier_formats != nil
48
+ name_id = []
49
+ name_identifier_formats.each_with_index{ |format, index|
50
+ name_id[index] = sp_sso.add_element "md:NameIDFormat"
51
+ name_id[index].text = format
52
+ }
53
+
54
+ end
55
+ if settings.sp_cert != nil
56
+ keyDescriptor = sp_sso.add_element "md:KeyDescriptor", {
57
+ "use" => "signing"
58
+ }
59
+ keyInfo = keyDescriptor.add_element "ds:KeyInfo", {
60
+ "xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"
61
+ }
62
+ x509Data = keyInfo.add_element "ds:X509Data"
63
+ x509Certificate = x509Data.add_element "ds:X509Certificate"
64
+ file = ""
65
+ File.foreach(settings.sp_cert){ |line|
66
+ file += line unless (line.include?("RSA PUBLIC KEY") || line.include?("CERTIFICATE"))
67
+ }
68
+ x509Certificate.text = file
69
+ end
70
+ if settings.assertion_consumer_service_url != nil
71
+ sp_sso.add_element "md:AssertionConsumerService", {
72
+ # Add this as a setting to create different bindings?
73
+ "Binding" => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
74
+ "Location" => settings.assertion_consumer_service_url,
75
+ "index" => "0",
76
+ "isDefault" => "true"
77
+ }
78
+ end
79
+ if settings.single_logout_service_url != nil
80
+ sp_sso.add_element "md:SingleLogoutService", {
81
+ "Binding" => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
82
+ "Location" => settings.single_logout_service_url
83
+ }
84
+ sp_sso.add_element "md:SingleLogoutService", {
85
+ "Binding" => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
86
+ "Location" => settings.single_logout_service_url
87
+ }
88
+ end
89
+ #AttributeConsumingService
90
+ attr_cons_service = sp_sso.add_element "md:AttributeConsumingService", {
91
+ "index" => "0",
92
+ "ServiceName" => "user_data"
93
+ }
94
+ settings.requested_attribute.each_with_index{ |attribute, index|
95
+ attr_cons_service.add_element "md:RequestedAttribute", {
96
+ "Name" => attribute
97
+ }
98
+ }
99
+
100
+ #organization
101
+ organization = root.add_element "md:Organization"
102
+ org_name = organization.add_element "md:OrganizationName", {
103
+ "xml:lang" => "it"
104
+ }
105
+ org_name.text = settings.organization['org_name']
106
+ org_display_name = organization.add_element "md:OrganizationDisplayName", {
107
+ "xml:lang" => "it"
108
+ }
109
+ org_display_name.text = settings.organization['org_display_name']
110
+ org_url = organization.add_element "md:OrganizationURL", {
111
+ "xml:lang" => "it"
112
+ }
113
+ org_url.text = settings.organization['org_url']
114
+
115
+ #meta_doc << REXML::XMLDecl.new(version='1.0', encoding='UTF-8')
116
+ meta_doc << REXML::XMLDecl.new("1.0", "UTF-8")
117
+
118
+ cert = settings.get_sp_cert
119
+ #SE SERVE ANCHE ENCRYPTION
120
+ # # Add KeyDescriptor if messages will be signed / encrypted
121
+ #
122
+ # if cert
123
+ # cert_text = Base64.encode64(cert.to_der).gsub("\n", '')
124
+ # kd = sp_sso.add_element "md:KeyDescriptor", { "use" => "signing" }
125
+ # ki = kd.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"}
126
+ # xd = ki.add_element "ds:X509Data"
127
+ # xc = xd.add_element "ds:X509Certificate"
128
+ # xc.text = cert_text
129
+
130
+ # kd2 = sp_sso.add_element "md:KeyDescriptor", { "use" => "encryption" }
131
+ # ki2 = kd2.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"}
132
+ # xd2 = ki2.add_element "ds:X509Data"
133
+ # xc2 = xd2.add_element "ds:X509Certificate"
134
+ # xc2.text = cert_text
135
+ # end
136
+
137
+ # embed signature
138
+ if settings.metadata_signed && settings.sp_private_key && settings.sp_cert
139
+ private_key = settings.get_sp_key
140
+
141
+ meta_doc.sign_document(private_key, cert)
142
+ end
143
+
144
+
145
+
146
+ ret = ""
147
+ # pretty print the XML so IdP administrators can easily see what the SP supports
148
+ meta_doc.write(ret, 1)
149
+
150
+ #Logging.debug "Generated metadata:\n#{ret}"
151
+
152
+ return ret
153
+
154
+ end
155
+
156
+ def create_sso_request(message, extra_parameters = {} )
157
+ build_message( :type => "SAMLRequest",
158
+ :service => "SingleSignOnService",
159
+ :message => message, :extra_parameters => extra_parameters)
160
+ end
161
+ def create_sso_response(message, extra_parameters = {} )
162
+ build_message( :type => "SAMLResponse",
163
+ :service => "SingleSignOnService",
164
+ :message => message, :extra_parameters => extra_parameters)
165
+ end
166
+ def create_slo_request(message, extra_parameters = {} )
167
+ build_message( :type => "SAMLRequest",
168
+ :service => "SingleLogoutService",
169
+ :message => message, :extra_parameters => extra_parameters)
170
+ end
171
+ def create_slo_response(message, extra_parameters = {} )
172
+ build_message( :type => "SAMLResponse",
173
+ :service => "SingleLogoutService",
174
+ :message => message, :extra_parameters => extra_parameters)
175
+ end
176
+
177
+ # Construct a SAML message using information in the IdP metadata.
178
+ # :type can be either "SAMLRequest" or "SAMLResponse"
179
+ # :service refers to the Binding method,
180
+ # either "SingleLogoutService" or "SingleSignOnService"
181
+ # :message is the SAML message itself (XML)
182
+ # I've provided easy to use wrapper functions above
183
+ def build_message( options = {} )
184
+ opt = { :type => nil, :service => nil, :message => nil, :extra_parameters => nil }.merge(options)
185
+ url = binding_select( opt[:service] )
186
+ return message_get( opt[:type], url, opt[:message], opt[:extra_parameters] )
187
+ end
188
+
189
+ # get the IdP metadata, and select the appropriate SSO binding
190
+ # that we can support. Currently this is HTTP-Redirect and HTTP-POST
191
+ # but more could be added in the future
192
+ def binding_select(service)
193
+ # first check if we're still using the old hard coded method for
194
+ # backwards compatability
195
+ if service == "SingleSignOnService" && @settings.idp_metadata == nil && @settings.idp_sso_target_url != nil
196
+ return @settings.idp_sso_target_url
197
+ end
198
+ if service == "SingleLogoutService" && @settings.idp_metadata == nil && @settings.idp_slo_target_url != nil
199
+ return @settings.idp_slo_target_url
200
+ end
201
+
202
+ meta_doc = get_idp_metadata
203
+
204
+ return nil unless meta_doc
205
+ # first try POST
206
+ sso_element = REXML::XPath.first(meta_doc, "/EntityDescriptor/IDPSSODescriptor/#{service}[@Binding='#{HTTP_POST}']")
207
+ if !sso_element.nil?
208
+ @URL = sso_element.attributes["Location"]
209
+ #Logging.debug "binding_select: POST to #{@URL}"
210
+ return @URL
211
+ end
212
+
213
+ # next try GET
214
+ sso_element = REXML::XPath.first(meta_doc, "/EntityDescriptor/IDPSSODescriptor/#{service}[@Binding='#{HTTP_GET}']")
215
+ if !sso_element.nil?
216
+ @URL = sso_element.attributes["Location"]
217
+ Logging.debug "binding_select: GET from #{@URL}"
218
+ return @URL
219
+ end
220
+ # other types we might want to add in the future: SOAP, Artifact
221
+ end
222
+
223
+
224
+ def fetch(uri_str, limit = 10)
225
+ # You should choose a better exception.
226
+ raise ArgumentError, 'too many HTTP redirects' if limit == 0
227
+
228
+ uri = URI.parse(uri_str)
229
+ if uri.scheme == "http"
230
+ response = Net::HTTP.get_response(uri)
231
+ elsif uri.scheme == "https"
232
+ http = Net::HTTP.new(uri.host, uri.port)
233
+ http.use_ssl = true
234
+ # Most IdPs will probably use self signed certs
235
+ #http.verify_mode = OpenSSL::SSL::VERIFY_PEER
236
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
237
+ get = Net::HTTP::Get.new(uri.request_uri)
238
+ response = http.request(get)
239
+ end
240
+
241
+ case response
242
+ when Net::HTTPSuccess then
243
+ response
244
+ when Net::HTTPRedirection then
245
+ location = response['location']
246
+ warn "redirected to #{location}"
247
+ fetch(location, limit - 1)
248
+ else
249
+ response.value
250
+ end
251
+ end
252
+
253
+
254
+
255
+ # Retrieve the remote IdP metadata from the URL or a cached copy
256
+ # returns a REXML document of the metadata
257
+ def get_idp_metadata
258
+ return false if @settings.idp_metadata.nil?
259
+
260
+ # Look up the metdata in cache first
261
+ id = Digest::MD5.hexdigest(@settings.idp_metadata)
262
+ response = fetch(@settings.idp_metadata)
263
+ #meta_text = response.body
264
+ #testo_response = meta_text.sub!(' xmlns:xml="http://www.w3.org/XML/1998/namespace"', '') da errori
265
+ #uso nokogiri per cercare il certificato, uso la funzione che rimuove tutti i namespace
266
+ doc_noko = Nokogiri::XML(response.body)
267
+ doc_noko.remove_namespaces!
268
+ extract_certificate(doc_noko)
269
+ doc_rexml = REXML::Document.new(doc_noko.to_xml)
270
+
271
+ return doc_rexml
272
+
273
+ # USE OF CACHE WITH CERTIFICATE
274
+ # lookup = @cache.read(id)
275
+ # if lookup != nil
276
+ # Logging.debug "IdP metadata cached lookup for #{@settings.idp_metadata}"
277
+ # doc = REXML::Document.new( lookup )
278
+ # extract_certificate( doc )
279
+ # return doc
280
+ # end
281
+
282
+ # Logging.debug "IdP metadata cache miss on #{@settings.idp_metadata}"
283
+ # # cache miss
284
+ # if File.exists?(@settings.idp_metadata)
285
+ # fp = File.open( @settings.idp_metadata, "r")
286
+ # meta_text = fp.read
287
+ # else
288
+ # uri = URI.parse(@settings.idp_metadata)
289
+ # if uri.scheme == "http"
290
+ # response = Net::HTTP.get_response(uri)
291
+ # meta_text = response.body
292
+ # elsif uri.scheme == "https"
293
+ # http = Net::HTTP.new(uri.host, uri.port)
294
+ # http.use_ssl = true
295
+ # # Most IdPs will probably use self signed certs
296
+ # #http.verify_mode = OpenSSL::SSL::VERIFY_PEER
297
+ # http.verify_mode = OpenSSL::SSL::VERIFY_NONE
298
+ # get = Net::HTTP::Get.new(uri.request_uri)
299
+ # response = http.request(get)
300
+ # meta_text = response.body
301
+ # end
302
+ # end
303
+ # # Add it to the cache
304
+ # @cache.write(id, meta_text, @settings.idp_metadata_ttl )
305
+ # doc = REXML::Document.new( meta_text )
306
+ # extract_certificate(doc)
307
+ # return doc
308
+ end
309
+
310
+ def extract_certificate(meta_doc)
311
+ #ricerco il certificato con nokogiri
312
+ # pull out the x509 tag
313
+ x509 = meta_doc.xpath("//EntityDescriptor//IDPSSODescriptor//KeyDescriptor//KeyInfo//X509Data//X509Certificate")
314
+
315
+ #x509 = REXML::XPath.first(meta_doc, "/md:EntityDescriptor/md:IDPSSODescriptor"+"/md:KeyDescriptor"+"/ds:KeyInfo/ds:X509Data/ds:X509Certificate")
316
+ # If the IdP didn't specify the use attribute
317
+ if x509.nil?
318
+ x509 = meta_doc.xpath("//EntityDescriptor//IDPSSODescriptor//KeyDescriptor//KeyInfo//X509Data//X509Certificate")
319
+ # x509 = REXML::XPath.first(meta_doc,
320
+ # "/EntityDescriptor/IDPSSODescriptor" +
321
+ # "/KeyDescriptor" +
322
+ # "/ds:KeyInfo/ds:X509Data/ds:X509Certificate"
323
+ # )
324
+ end
325
+ @settings.idp_cert = x509.children.to_s.gsub(/\n/, "").gsub(/\t/, "")
326
+ end
327
+
328
+ # construct the parameter list on the URL and return
329
+ def message_get( type, url, message, extra_parameters = {} )
330
+ params = Hash.new
331
+ if extra_parameters
332
+ params.merge!(extra_parameters)
333
+ end
334
+ # compress GET requests to try and stay under that 8KB request limit
335
+ #deflate of samlrequest
336
+ params[type] = encode( deflate( message ) )
337
+ #Logging.debug "#{type}=#{params[type]}"
338
+
339
+ uri = Addressable::URI.parse(url)
340
+ if uri.query_values == nil
341
+ uri.query_values = params
342
+ else
343
+ # solution to stevenwilkin's parameter merge
344
+ uri.query_values = params.merge(uri.query_values)
345
+ end
346
+ url = uri.to_s
347
+ #Logging.debug "Sending to URL #{url}"
348
+ return url
349
+ end
350
+
351
+ end
352
+ end
353
+ end
@@ -0,0 +1,81 @@
1
+
2
+ # A few helper functions for assembling a SAMLRequest and
3
+ # sending it to the IdP
4
+ module Spid::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