cie-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 (50) hide show
  1. checksums.yaml +7 -0
  2. data/.document +5 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +19 -0
  5. data/README.md +126 -0
  6. data/Rakefile +41 -0
  7. data/cie-es.gemspec +22 -0
  8. data/lib/cie/ruby-saml/authrequest.rb +205 -0
  9. data/lib/cie/ruby-saml/coding.rb +34 -0
  10. data/lib/cie/ruby-saml/error_handling.rb +27 -0
  11. data/lib/cie/ruby-saml/logging.rb +26 -0
  12. data/lib/cie/ruby-saml/logout_request.rb +126 -0
  13. data/lib/cie/ruby-saml/logout_response.rb +132 -0
  14. data/lib/cie/ruby-saml/metadata.rb +489 -0
  15. data/lib/cie/ruby-saml/request.rb +81 -0
  16. data/lib/cie/ruby-saml/response.rb +678 -0
  17. data/lib/cie/ruby-saml/settings.rb +89 -0
  18. data/lib/cie/ruby-saml/utils.rb +225 -0
  19. data/lib/cie/ruby-saml/validation_error.rb +7 -0
  20. data/lib/cie/ruby-saml/version.rb +5 -0
  21. data/lib/cie/xml_security.rb +166 -0
  22. data/lib/cie/xml_security_new.rb +373 -0
  23. data/lib/cie-es.rb +14 -0
  24. data/lib/schemas/saml20assertion_schema.xsd +283 -0
  25. data/lib/schemas/saml20protocol_schema.xsd +302 -0
  26. data/lib/schemas/xenc_schema.xsd +146 -0
  27. data/lib/schemas/xmldsig_schema.xsd +318 -0
  28. data/test/certificates/certificate1 +12 -0
  29. data/test/logoutrequest_test.rb +98 -0
  30. data/test/request_test.rb +53 -0
  31. data/test/response_test.rb +219 -0
  32. data/test/responses/adfs_response_sha1.xml +46 -0
  33. data/test/responses/adfs_response_sha256.xml +46 -0
  34. data/test/responses/adfs_response_sha384.xml +46 -0
  35. data/test/responses/adfs_response_sha512.xml +46 -0
  36. data/test/responses/no_signature_ns.xml +48 -0
  37. data/test/responses/open_saml_response.xml +56 -0
  38. data/test/responses/response1.xml.base64 +1 -0
  39. data/test/responses/response2.xml.base64 +79 -0
  40. data/test/responses/response3.xml.base64 +66 -0
  41. data/test/responses/response4.xml.base64 +93 -0
  42. data/test/responses/response5.xml.base64 +102 -0
  43. data/test/responses/response_with_ampersands.xml +139 -0
  44. data/test/responses/response_with_ampersands.xml.base64 +93 -0
  45. data/test/responses/simple_saml_php.xml +71 -0
  46. data/test/responses/wrapped_response_2.xml.base64 +150 -0
  47. data/test/settings_test.rb +43 -0
  48. data/test/test_helper.rb +65 -0
  49. data/test/xml_security_test.rb +123 -0
  50. metadata +119 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1fddb69bc580ee945fdf740e072a06a43f13133bc821e65cd4c16ebde05a8490
4
+ data.tar.gz: 1ebb454f25165f5916887b4742eeaf0e8a1c9b823bd2377a8c5b12e70d4ac595
5
+ SHA512:
6
+ metadata.gz: 5f6a34790df5ebf8d9d0bf06e71167e34550b2c972cf113c881d92a86dd96bae9b164b15c2891e20ab3a3f0c67a6c8f18906fcec5c8baca0cf287b352e209c0e
7
+ data.tar.gz: e352e080c27f29fc4b214d99466ff75d5e38f2d680fad4d60140865084038d89c308e60c44b804abafbddb961a6c4b4017eb6cbd00de9b1521521be028b531e5
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gemspec
4
+
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2010 OneLogin, LLC
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # CIE Euro Servizi
2
+
3
+ La libreria è un fork della libreria Ruby SAML e serve per l'integrazione di un client (Service Provider) con l'autenticazione CIE (Carta d'Identità Elettronica)
4
+ Utilizza lo standard SAML 2 come previsto dalla normativa
5
+
6
+ ## Fase iniziale
7
+
8
+ Azione di partenza in cui viene creata la request da inviare all'idp e viene fatto un redirect all'identity provider.
9
+
10
+ ```ruby
11
+ def init
12
+ #creo un istanza di Cie::Saml::Authrequest
13
+ saml_settings = get_saml_settings
14
+ #create an instance of Cie::Saml::Authrequest
15
+ request = Cie::Saml::Authrequest.new(saml_settings)
16
+ auth_request = request.create
17
+ # Based on the IdP metadata, select the appropriate binding
18
+ # and return the action to perform to the controller
19
+ meta = Cie::Saml::Metadata.new(saml_settings)
20
+ signature = get_signature(auth_request.uuid,auth_request.request,"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256")
21
+ sso_request = meta.create_sso_request( auth_request.request, { :RelayState => request.uuid,
22
+ :SigAlg => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
23
+ :Signature => signature } )
24
+ redirect_to sso_request
25
+ end
26
+
27
+ ```
28
+ ## Generazione della firma
29
+
30
+ ```ruby
31
+ def get_signature(relayState, request, sigAlg)
32
+ #url encode relayState
33
+ relayState_encoded = escape(relayState)
34
+ #deflate e base64 della samlrequest
35
+ deflate_request_B64 = encode(deflate(request))
36
+ #url encode della samlrequest
37
+ deflate_request_B64_encoded = escape(deflate_request_B64)
38
+ #url encode della sigAlg
39
+ sigAlg_encoded = escape(sigAlg)
40
+ querystring="SAMLRequest=#{deflate_request_B64_encoded}&RelayState=#{relayState_encoded}&SigAlg=#{sigAlg_encoded}"
41
+ #puts "**QUERYSTRING** = "+querystring
42
+ digest = OpenSSL::Digest::SHA256.new(querystring.strip) #sha2 a 256
43
+ chiave_privata = xxxxxx #path della chiave privata con cui firmare
44
+ pk = OpenSSL::PKey::RSA.new File.read(chiave_privata) #chiave privata
45
+ qssigned = pk.sign(digest,querystring.strip)
46
+ Base64.encode64(qssigned).gsub(/\n/, "")
47
+ end
48
+ ```
49
+
50
+
51
+ Questo metodo è l'endpoint impostato a livello di metadata del service provider come 'assertion consumer', riceve la response saml con i dati della CIE degli utenti.
52
+
53
+ ```ruby
54
+ def assertion_consumer
55
+ #id dell' idp che manda response (es: 'infocert','poste')
56
+ provider_id = @request.params['ProviderID']
57
+ #response saml inviata dall'idp
58
+ saml_response = @request.params['SAMLResponse']
59
+ if !saml_response.nil?
60
+ #assegno i settaggi
61
+ settings = get_saml_settings
62
+ #creo un oggetto response
63
+ response = Cie::Saml::Response.new(saml_response)
64
+ #assegno alla response i settaggi
65
+ response.settings = settings
66
+ #estraggo dal Base64 l'xml
67
+ saml_response_dec = Base64.decode64(saml_response)
68
+ #puts "**SAML RESPONSE DECODIFICATA: #{saml_response_dec}"
69
+
70
+ #validation of response
71
+ if response.is_valid?
72
+ attributi_utente = response.attributes
73
+ ...
74
+ else
75
+ #autenticazione fallita!
76
+ end
77
+ end
78
+ end
79
+ ```
80
+
81
+ Questo metodo va a impostare le varie configurazioni che servono per connettersi all'idp.
82
+
83
+ ```ruby
84
+ def get_saml_settings
85
+ settings = Cie::Saml::Settings.new
86
+ settings.assertion_consumer_service_url #= ...String, url dell' assertion consumer al quale arriva la response dell' idp.
87
+ settings.issuer #= ...String, host del service provider o url dei metadata.
88
+ settings.sp_cert #= ...String, path del certificato pubblico in formato pem.
89
+ settings.sp_private_key #= ...String, path della chiave privata in formato pem.
90
+ settings.single_logout_service_url #= ...String, url del servizio di logout dell'idp.
91
+ settings.sp_name_qualifier #= ...String, nome qualificato del service provider o url dei metadata.
92
+ settings.idp_name_qualifier #= ...String, nome qualificato dell' identity provider o url dei metadata dell' idp.
93
+ settings.name_identifier_format #= ...Array, formato di nomi ( impostare: ["urn:oasis:names:tc:SAML:2.0:nameid-format:transient"] ).
94
+ settings.destination_service_url #= ...String, url del servizio per l'identity provider, usato come proxy per il sso.
95
+ settings.single_logout_destination #= ...String, url di destinazione per la request logout.
96
+ settings.authn_context #= ...Array, tipi di autorizzazioni permesse (impostare: ["urn:oasis:names:tc:SAML:2.0:ac:classes:Smartcard",
97
+ "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"]).
98
+ settings.requester_identificator #= ...Array con id dei richiedenti (non usato).
99
+ settings.skip_validation #= ...Bool, imposta se evitare la validazione della response o delle asserzioni (false).
100
+ settings.idp_sso_target_url #= ...String, url target del sso dell' identity provider.
101
+ settings.idp_metadata #= ...String, url dei metadata dell' idp.
102
+ settings.requested_attribute #= ...Array, contiene i nomi dei campi richiesti dal servizio nei metadata.
103
+ settings.metadata_signed #= ...String, imposta se firmare i metadata.
104
+ settings.organization #= ...Hash, contiene nome breve (org_name), nome esteso (org_display_name) e url (org_url)
105
+ dell' organizzazione fornitore di servizi.
106
+ settings
107
+ end
108
+ ```
109
+
110
+
111
+
112
+ ## Service Provider Metadata
113
+
114
+ Per una relazione sicura con l'idp, il Service Provider deve fornire i metadata in formato xml.
115
+ La classe Cie::Saml::Metadata legge i settaggi e fornisce l'xml richiesto dagli idp.
116
+
117
+ ```ruby
118
+ def sp_metadata
119
+ settings = get_saml_settings
120
+ meta = Cie::Saml::Metadata.new
121
+
122
+ @response.headers['Content-Type'] = 'application/samlmetadata+xml'
123
+ $out << meta.generate(settings)
124
+ end
125
+ ```
126
+
data/Rakefile ADDED
@@ -0,0 +1,41 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ #not being used yet.
5
+ require 'rake/testtask'
6
+ Rake::TestTask.new(:test) do |test|
7
+ test.libs << 'lib' << 'test'
8
+ test.pattern = 'test/**/*_test.rb'
9
+ test.verbose = true
10
+ end
11
+
12
+ begin
13
+ require 'rcov/rcovtask'
14
+ Rcov::RcovTask.new do |test|
15
+ test.libs << 'test'
16
+ test.pattern = 'test/**/*_test.rb'
17
+ test.verbose = true
18
+ end
19
+ rescue LoadError
20
+ task :rcov do
21
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
22
+ end
23
+ end
24
+
25
+ task :test
26
+
27
+ task :default => :test
28
+
29
+ # require 'rake/rdoctask'
30
+ # Rake::RDocTask.new do |rdoc|
31
+ # if File.exist?('VERSION')
32
+ # version = File.read('VERSION')
33
+ # else
34
+ # version = ""
35
+ # end
36
+
37
+ # rdoc.rdoc_dir = 'rdoc'
38
+ # rdoc.title = "ruby-saml #{version}"
39
+ # rdoc.rdoc_files.include('README*')
40
+ # rdoc.rdoc_files.include('lib/**/*.rb')
41
+ #end
data/cie-es.gemspec ADDED
@@ -0,0 +1,22 @@
1
+ $LOAD_PATH.push File.expand_path('../lib', __FILE__)
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'cie-es'
5
+ s.version = '0.0.1'
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Fabiano Pavan"]
9
+ s.date = Time.now.strftime("%Y-%m-%d")
10
+ s.description = "Libreria ruby per autenticazione con CIE"
11
+ s.email = %q{fabiano.pavan@soluzionipa.it}
12
+ s.files = `git ls-files`.split("\n")
13
+ s.homepage = %q{https://github.com/EuroServizi/cie-es}
14
+ s.rdoc_options = ["--charset=UTF-8"]
15
+ s.require_paths = ["lib"]
16
+ s.summary = %q{SAML Ruby Toolkit Cie}
17
+ s.license = "MIT"
18
+
19
+ s.add_runtime_dependency("canonix", ["0.1.1"])
20
+ s.add_runtime_dependency("uuid", ["~> 2.3"])
21
+
22
+ end
@@ -0,0 +1,205 @@
1
+ require "base64"
2
+ require "uuid"
3
+ require "zlib"
4
+ require "cgi"
5
+ require "rexml/document"
6
+ require "rexml/xpath"
7
+ require "rubygems"
8
+ require "addressable/uri"
9
+
10
+ module Cie::Saml
11
+ include REXML
12
+ class Authrequest
13
+ # a few symbols for SAML class names
14
+ HTTP_POST = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
15
+ HTTP_GET = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
16
+
17
+ attr_accessor :uuid, :request, :issue_instant
18
+
19
+ def initialize( settings )
20
+ @settings = settings
21
+ @request_params = Hash.new
22
+ end
23
+
24
+ def create(params = {})
25
+ uuid = "_" + UUID.new.generate
26
+ self.uuid = uuid
27
+ time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
28
+ self.issue_instant = time
29
+ # Create AuthnRequest root element using REXML
30
+ request_doc = Cie::XMLSecurityNew::Document.new
31
+ request_doc.context[:attribute_quote] = :quote
32
+ root = request_doc.add_element "saml2p:AuthnRequest", { "xmlns:saml2p" => "urn:oasis:names:tc:SAML:2.0:protocol",
33
+ "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion"
34
+ }
35
+ root.attributes['ID'] = uuid
36
+ root.attributes['IssueInstant'] = time
37
+ root.attributes['Version'] = "2.0"
38
+ #root.attributes['ProtocolBinding'] = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
39
+ root.attributes['AttributeConsumingServiceIndex'] = @settings.assertion_consumer_service_index
40
+ root.attributes['ForceAuthn'] = "true"
41
+ #root.attributes['IsPassive'] = "false"
42
+ #usato AssertionConsumerServiceURL e ProtocolBinding in alternativa, pag 8 regole tecniche
43
+ root.attributes['AssertionConsumerServiceIndex'] = @settings.attribute_consuming_service_index
44
+
45
+ #Tolto, utilizzo AssertionConsumerServiceIndex
46
+ # # Conditionally defined elements based on settings
47
+ # if @settings.assertion_consumer_service_url != nil
48
+ # root.attributes["AssertionConsumerServiceURL"] = @settings.assertion_consumer_service_url
49
+ # end
50
+
51
+ if @settings.destination_service_url != nil
52
+ root.attributes["Destination"] = @settings.destination_service_url
53
+ end
54
+
55
+ unless @settings.issuer.blank?
56
+ issuer = root.add_element "saml:Issuer"
57
+ issuer.attributes['NameQualifier'] = @settings.issuer #non metto @settings.sp_name_qualifier, questo valore deve essere uguale al
58
+ #entityID dei metadata che usa @settings.issuer
59
+ issuer.text = @settings.issuer
60
+ end
61
+
62
+ #opzionale
63
+ unless @settings.sp_name_qualifier.blank?
64
+ subject = root.add_element "saml:Subject"
65
+ name_id = subject.add_element "saml:NameID"
66
+ name_id.attributes['Format'] = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
67
+ name_id.attributes['NameQualifier'] = @settings.sp_name_qualifier
68
+ name_id.text = @settings.sp_name_identifier
69
+ end
70
+
71
+
72
+
73
+ if @settings.name_identifier_format != nil
74
+ root.add_element "saml2p:NameIDPolicy", {
75
+ # Might want to make AllowCreate a setting?
76
+ #{}"AllowCreate" => "true",
77
+ "Format" => @settings.name_identifier_format[0]
78
+ }
79
+ end
80
+
81
+ # BUG fix here -- if an authn_context is defined, add the tags with an "exact"
82
+ # match required for authentication to succeed. If this is not defined,
83
+ # the IdP will choose default rules for authentication. (Shibboleth IdP)
84
+ if @settings.authn_context != nil
85
+ requested_context = root.add_element "saml2p:RequestedAuthnContext", {
86
+ "Comparison" => "minimum"
87
+ }
88
+ context_class = []
89
+ @settings.authn_context.each_with_index{ |context, index|
90
+ context_class[index] = requested_context.add_element "saml:AuthnContextClassRef"
91
+ context_class[index].text = context
92
+ }
93
+
94
+ end
95
+
96
+ if @settings.requester_identificator != nil
97
+ requester_identificator = root.add_element "saml2p:Scoping", {
98
+ "ProxyCount" => "0"
99
+ }
100
+ identificators = []
101
+ @settings.requester_identificator.each_with_index{ |requester, index|
102
+ identificators[index] = requester_identificator.add_element "saml2p:RequesterID"
103
+ identificators[index].text = requester
104
+ }
105
+
106
+ end
107
+
108
+ request_doc << REXML::XMLDecl.new("1.0", "UTF-8")
109
+
110
+ #LA FIRMA VA MESSA SOLO NEL CASO CON HTTP POST
111
+ # cert = @settings.get_sp_cert
112
+ # # embed signature
113
+ # if @settings.metadata_signed && @settings.sp_private_key && @settings.sp_cert
114
+ # private_key = @settings.get_sp_key
115
+ # request_doc.sign_document(private_key, cert)
116
+ # end
117
+
118
+ # stampo come stringa semplice i metadata per non avere problemi con validazione firma
119
+ #ret = request_doc.to_s
120
+
121
+ @request = request_doc.to_s
122
+
123
+ #Logging.debug "Created AuthnRequest: #{@request}"
124
+
125
+ return self
126
+
127
+ end
128
+
129
+ # get the IdP metadata, and select the appropriate SSO binding
130
+ # that we can support. Currently this is HTTP-Redirect and HTTP-POST
131
+ # but more could be added in the future
132
+ def binding_select
133
+ # first check if we're still using the old hard coded method for
134
+ # backwards compatability
135
+ if @settings.idp_metadata == nil && @settings.idp_sso_target_url != nil
136
+ @URL = @settings.idp_sso_target_url
137
+ return "GET", content_get
138
+ end
139
+ # grab the metadata
140
+ metadata = Metadata::new
141
+ meta_doc = metadata.get_idp_metadata(@settings)
142
+
143
+ # first try POST
144
+ sso_element = REXML::XPath.first(meta_doc,
145
+ "/EntityDescriptor/IDPSSODescriptor/SingleSignOnService[@Binding='#{HTTP_POST}']")
146
+ if sso_element
147
+ @URL = sso_element.attributes["Location"]
148
+ #Logging.debug "binding_select: POST to #{@URL}"
149
+ return "POST", content_post
150
+ end
151
+
152
+ # next try GET
153
+ sso_element = REXML::XPath.first(meta_doc,
154
+ "/EntityDescriptor/IDPSSODescriptor/SingleSignOnService[@Binding='#{HTTP_GET}']")
155
+ if sso_element
156
+ @URL = sso_element.attributes["Location"]
157
+ Logging.debug "binding_select: GET from #{@URL}"
158
+ return "GET", content_get
159
+ end
160
+ # other types we might want to add in the future: SOAP, Artifact
161
+ end
162
+
163
+ # construct the the parameter list on the URL and return
164
+ def content_get
165
+ # compress GET requests to try and stay under that 8KB request limit
166
+ deflated_request = Zlib::Deflate.deflate(@request, 9)[2..-5]
167
+ # strict_encode64() isn't available? sub out the newlines
168
+ @request_params["SAMLRequest"] = Base64.encode64(deflated_request).gsub(/\n/, "")
169
+
170
+ Logging.debug "SAMLRequest=#{@request_params["SAMLRequest"]}"
171
+ uri = Addressable::URI.parse(@URL)
172
+ if uri.query_values == nil
173
+ uri.query_values = @request_params
174
+ else
175
+ # solution to stevenwilkin's parameter merge
176
+ uri.query_values = @request_params.merge(uri.query_values)
177
+ end
178
+ url = uri.to_s
179
+ #Logging.debug "Sending to URL #{url}"
180
+ return url
181
+ end
182
+ # construct an HTML form (POST) and return the content
183
+ def content_post
184
+ # POST requests seem to bomb out when they're deflated
185
+ # and they probably don't need to be compressed anyway
186
+ @request_params["SAMLRequest"] = Base64.encode64(@request).gsub(/\n/, "")
187
+
188
+ #Logging.debug "SAMLRequest=#{@request_params["SAMLRequest"]}"
189
+ # kind of a cheesy method of building an HTML, form since we can't rely on Rails too much,
190
+ # and REXML doesn't work well with quote characters
191
+ str = "<html><body onLoad=\"document.getElementById('form').submit();\">\n"
192
+ str += "<form id='form' name='form' method='POST' action=\"#{@URL}\">\n"
193
+ # we could change this in the future to associate a temp auth session ID
194
+ str += "<input name='RelayState' value='ruby-saml' type='hidden' />\n"
195
+ @request_params.each_pair do |key, value|
196
+ str += "<input name=\"#{key}\" value=\"#{value}\" type='hidden' />\n"
197
+ #str += "<input name=\"#{key}\" value=\"#{CGI.escape(value)}\" type='hidden' />\n"
198
+ end
199
+ str += "</form></body></html>\n"
200
+
201
+ #Logging.debug "Created form:\n#{str}"
202
+ return str
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,34 @@
1
+ require "cgi"
2
+ require 'zlib'
3
+
4
+ module Cie
5
+ module Saml
6
+ module Coding
7
+ def decode(encoded)
8
+ Base64.decode64(encoded)
9
+ end
10
+
11
+ def encode(encoded)
12
+ Base64.strict_encode64(encoded)
13
+ end
14
+
15
+ def escape(unescaped)
16
+ CGI.escape(unescaped)
17
+ end
18
+
19
+ def unescape(escaped)
20
+ CGI.unescape(escaped)
21
+ end
22
+
23
+ def inflate(deflated)
24
+ zlib = Zlib::Inflate.new(-Zlib::MAX_WBITS)
25
+ zlib.inflate(deflated)
26
+ end
27
+
28
+ def deflate(inflated)
29
+ Zlib::Deflate.deflate(inflated, 9)[2..-5]
30
+ end
31
+
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,27 @@
1
+ require "cie/ruby-saml/validation_error"
2
+
3
+ module Cie
4
+ module Saml
5
+ module ErrorHandling
6
+ attr_accessor :errors
7
+
8
+ # Append the cause to the errors array, and based on the value of soft, return false or raise
9
+ # an exception. soft_override is provided as a means of overriding the object's notion of
10
+ # soft for just this invocation.
11
+ def append_error(error_msg, soft_override = nil)
12
+ @errors << error_msg
13
+
14
+ unless soft_override.nil? ? soft : soft_override
15
+ raise ValidationError.new(error_msg)
16
+ end
17
+
18
+ false
19
+ end
20
+
21
+ # Reset the errors array
22
+ def reset_errors!
23
+ @errors = []
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,26 @@
1
+ # Simplistic log class when we're running in Rails
2
+ module Cie
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 Cie::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 = Cie::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 = Cie::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 = Cie::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