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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4e95196cb69850d66b4ad960fcf279885cb600448b39548e22b627998da49363
4
+ data.tar.gz: 53895057eaafde1db9a846ca1f885219348bdeb0dcaffffb29c39c74e78bd20c
5
+ SHA512:
6
+ metadata.gz: 12312ab615271d5e7213a49528de7a286ffd3a103bdd7cf4ff810c2f146fe5bd7fba0bb10df24a37a7c9d3c74be0c875d43fdaaa3e05bead86c1366545278918
7
+ data.tar.gz: 2f46ce928a88d79369d7b22d8ef29f4490b0a6a8c600c2927b511e539d8b62a189c3c81ff56a3276b965d907d2f0ea78890c28594c9a71a98c092fd6a5749f85
@@ -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
+
@@ -0,0 +1,127 @@
1
+ # SPID 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 CIAM del Comune di Milano
4
+ Utilizza lo standard SAML 2
5
+
6
+
7
+ <!-- ## Fase iniziale
8
+
9
+ Azione di partenza in cui viene creata la request da inviare all'idp e viene fatto un redirect all'identity provider.
10
+
11
+ ```ruby
12
+ def init
13
+ #creo un istanza di Spid::Saml::Authrequest
14
+ saml_settings = get_saml_settings
15
+ #create an instance of Spid::Saml::Authrequest
16
+ request = Spid::Saml::Authrequest.new(saml_settings)
17
+ auth_request = request.create
18
+ # Based on the IdP metadata, select the appropriate binding
19
+ # and return the action to perform to the controller
20
+ meta = Spid::Saml::Metadata.new(saml_settings)
21
+ signature = get_signature(auth_request.uuid,auth_request.request,"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256")
22
+ sso_request = meta.create_sso_request( auth_request.request, { :RelayState => request.uuid,
23
+ :SigAlg => "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
24
+ :Signature => signature } )
25
+ redirect_to sso_request
26
+ end
27
+
28
+ ```
29
+ ## Generazione della firma
30
+
31
+ ```ruby
32
+ def get_signature(relayState, request, sigAlg)
33
+ #url encode relayState
34
+ relayState_encoded = escape(relayState)
35
+ #deflate e base64 della samlrequest
36
+ deflate_request_B64 = encode(deflate(request))
37
+ #url encode della samlrequest
38
+ deflate_request_B64_encoded = escape(deflate_request_B64)
39
+ #url encode della sigAlg
40
+ sigAlg_encoded = escape(sigAlg)
41
+ querystring="SAMLRequest=#{deflate_request_B64_encoded}&RelayState=#{relayState_encoded}&SigAlg=#{sigAlg_encoded}"
42
+ #puts "**QUERYSTRING** = "+querystring
43
+ digest = OpenSSL::Digest::SHA256.new(querystring.strip) #sha2 a 256
44
+ chiave_privata = xxxxxx #path della chiave privata con cui firmare
45
+ pk = OpenSSL::PKey::RSA.new File.read(chiave_privata) #chiave privata
46
+ qssigned = pk.sign(digest,querystring.strip)
47
+ Base64.encode64(qssigned).gsub(/\n/, "")
48
+ end
49
+ ```
50
+
51
+
52
+ Questo metodo è l'endpoint impostato a livello di metadata del service provider come 'assertion consumer', riceve la response saml con i dati di registrazione fatta su SPID dagli utenti.
53
+
54
+ ```ruby
55
+ def assertion_consumer
56
+ #id dell' idp che manda response (es: 'infocert','poste')
57
+ provider_id = @request.params['ProviderID']
58
+ #response saml inviata dall'idp
59
+ saml_response = @request.params['SAMLResponse']
60
+ if !saml_response.nil?
61
+ #assegno i settaggi
62
+ settings = get_saml_settings
63
+ #creo un oggetto response
64
+ response = Spid::Saml::Response.new(saml_response)
65
+ #assegno alla response i settaggi
66
+ response.settings = settings
67
+ #estraggo dal Base64 l'xml
68
+ saml_response_dec = Base64.decode64(saml_response)
69
+ #puts "**SAML RESPONSE DECODIFICATA: #{saml_response_dec}"
70
+
71
+ #validation of response
72
+ if response.is_valid?
73
+ attributi_utente = response.attributes
74
+ ...
75
+ else
76
+ #autenticazione fallita!
77
+ end
78
+ end
79
+ end
80
+ ```
81
+
82
+ Questo metodo va a impostare le varie configurazioni che servono per connettersi ad un idp. ( NB: nel caso di SPID ci sono vari idp (Poste, TIM, Info Cert) )
83
+
84
+ ```ruby
85
+ def get_saml_settings
86
+ settings = Spid::Saml::Settings.new
87
+ settings.assertion_consumer_service_url #= ...String, url dell' assertion consumer al quale arriva la response dell' idp.
88
+ settings.issuer #= ...String, host del service provider o url dei metadata.
89
+ settings.sp_cert #= ...String, path del certificato pubblico in formato pem.
90
+ settings.sp_private_key #= ...String, path della chiave privata in formato pem.
91
+ settings.single_logout_service_url #= ...String, url del servizio di logout dell'idp.
92
+ settings.sp_name_qualifier #= ...String, nome qualificato del service provider o url dei metadata.
93
+ settings.idp_name_qualifier #= ...String, nome qualificato dell' identity provider o url dei metadata dell' idp.
94
+ settings.name_identifier_format #= ...Array, formato di nomi ( impostare: ["urn:oasis:names:tc:SAML:2.0:nameid-format:transient"] ).
95
+ settings.destination_service_url #= ...String, url del servizio per l'identity provider, usato come proxy per il sso.
96
+ settings.single_logout_destination #= ...String, url di destinazione per la request logout.
97
+ settings.authn_context #= ...Array, tipi di autorizzazioni permesse (impostare: ["urn:oasis:names:tc:SAML:2.0:ac:classes:Smartcard",
98
+ "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"]).
99
+ settings.requester_identificator #= ...Array con id dei richiedenti (non usato).
100
+ settings.skip_validation #= ...Bool, imposta se evitare la validazione della response o delle asserzioni (false).
101
+ settings.idp_sso_target_url #= ...String, url target del sso dell' identity provider.
102
+ settings.idp_metadata #= ...String, url dei metadata dell' idp.
103
+ settings.requested_attribute #= ...Array, contiene i nomi dei campi richiesti dal servizio nei metadata.
104
+ settings.metadata_signed #= ...String, imposta se firmare i metadata.
105
+ settings.organization #= ...Hash, contiene nome breve (org_name), nome esteso (org_display_name) e url (org_url)
106
+ dell' organizzazione fornitore di servizi.
107
+ settings
108
+ end
109
+ ```
110
+
111
+
112
+
113
+ ## Service Provider Metadata
114
+
115
+ Per una relazione sicura con l'idp, il Service Provider deve fornire i metadata in formato xml.
116
+ La classe Spid::Saml::Metadata legge i settaggi e fornisce l'xml richiesto dagli idp.
117
+
118
+ ```ruby
119
+ def sp_metadata
120
+ settings = get_saml_settings
121
+ meta = Spid::Saml::Metadata.new
122
+
123
+ @response.headers['Content-Type'] = 'application/samlmetadata+xml'
124
+ $out << meta.generate(settings)
125
+ end
126
+ ``` -->
127
+
@@ -0,0 +1,23 @@
1
+ $LOAD_PATH.push File.expand_path('../lib', __FILE__)
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'ciam-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 = %q{SAML toolkit for Ruby programs to integrate with CIAM Milano }
11
+ s.email = %q{fabiano.pavan@soluzionipa.it}
12
+ s.files = `git ls-files`.split("\n")
13
+ s.homepage = %q{https://github.com/EuroServizi/ciam-es}
14
+ s.rdoc_options = ["--charset=UTF-8"]
15
+ s.require_paths = ["lib"]
16
+ s.summary = %q{SAML Ruby Tookit CIAM}
17
+ s.license = "MIT"
18
+
19
+ s.add_runtime_dependency("canonix", ["0.1.1"])
20
+ s.add_runtime_dependency("uuid", ["~> 2.3"])
21
+ s.add_runtime_dependency("nokogiri", '>= 1.6.7.2')
22
+ s.add_runtime_dependency("addressable", ["2.7.0"])
23
+ end
@@ -0,0 +1,14 @@
1
+ require 'ciam/xml_security'
2
+ require 'ciam/ruby-saml/utils'
3
+ require 'ciam/ruby-saml/logging'
4
+ require 'ciam/ruby-saml/coding'
5
+ require 'ciam/ruby-saml/request'
6
+ require 'ciam/ruby-saml/authrequest'
7
+ require 'ciam/ruby-saml/logout_request'
8
+ require 'ciam/ruby-saml/logout_response'
9
+ require 'ciam/ruby-saml/response'
10
+ require 'ciam/ruby-saml/settings'
11
+ require 'ciam/ruby-saml/error_handling'
12
+ require 'ciam/ruby-saml/validation_error'
13
+ require 'ciam/ruby-saml/metadata'
14
+ require 'ciam/ruby-saml/version'
@@ -0,0 +1,206 @@
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 Ciam::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 = Ciam::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.attributes['Format'] = "urn:oasis:names:tc:SAML:2.0:nameid-format:entity"
60
+ issuer.text = @settings.issuer
61
+ end
62
+
63
+ #opzionale
64
+ unless @settings.sp_name_qualifier.blank?
65
+ subject = root.add_element "saml:Subject"
66
+ name_id = subject.add_element "saml:NameID"
67
+ name_id.attributes['Format'] = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
68
+ name_id.attributes['NameQualifier'] = @settings.sp_name_qualifier
69
+ name_id.text = @settings.sp_name_identifier
70
+ end
71
+
72
+
73
+
74
+ if @settings.name_identifier_format != nil
75
+ root.add_element "saml2p:NameIDPolicy", {
76
+ # Might want to make AllowCreate a setting?
77
+ #{}"AllowCreate" => "true",
78
+ "Format" => @settings.name_identifier_format[0]
79
+ }
80
+ end
81
+
82
+ # BUG fix here -- if an authn_context is defined, add the tags with an "exact"
83
+ # match required for authentication to succeed. If this is not defined,
84
+ # the IdP will choose default rules for authentication. (Shibboleth IdP)
85
+ if @settings.authn_context != nil
86
+ requested_context = root.add_element "saml2p:RequestedAuthnContext", {
87
+ "Comparison" => "minimum"
88
+ }
89
+ context_class = []
90
+ @settings.authn_context.each_with_index{ |context, index|
91
+ context_class[index] = requested_context.add_element "saml:AuthnContextClassRef"
92
+ context_class[index].text = context
93
+ }
94
+
95
+ end
96
+
97
+ if @settings.requester_identificator != nil
98
+ requester_identificator = root.add_element "saml2p:Scoping", {
99
+ "ProxyCount" => "0"
100
+ }
101
+ identificators = []
102
+ @settings.requester_identificator.each_with_index{ |requester, index|
103
+ identificators[index] = requester_identificator.add_element "saml2p:RequesterID"
104
+ identificators[index].text = requester
105
+ }
106
+
107
+ end
108
+
109
+ request_doc << REXML::XMLDecl.new("1.0", "UTF-8")
110
+
111
+ #LA FIRMA VA MESSA SOLO NEL CASO CON HTTP POST
112
+ # cert = @settings.get_sp_cert
113
+ # # embed signature
114
+ # if @settings.metadata_signed && @settings.sp_private_key && @settings.sp_cert
115
+ # private_key = @settings.get_sp_key
116
+ # request_doc.sign_document(private_key, cert)
117
+ # end
118
+
119
+ # stampo come stringa semplice i metadata per non avere problemi con validazione firma
120
+ #ret = request_doc.to_s
121
+
122
+ @request = request_doc.to_s
123
+
124
+ #Logging.debug "Created AuthnRequest: #{@request}"
125
+
126
+ return self
127
+
128
+ end
129
+
130
+ # get the IdP metadata, and select the appropriate SSO binding
131
+ # that we can support. Currently this is HTTP-Redirect and HTTP-POST
132
+ # but more could be added in the future
133
+ def binding_select
134
+ # first check if we're still using the old hard coded method for
135
+ # backwards compatability
136
+ if @settings.idp_metadata == nil && @settings.idp_sso_target_url != nil
137
+ @URL = @settings.idp_sso_target_url
138
+ return "GET", content_get
139
+ end
140
+ # grab the metadata
141
+ metadata = Metadata::new
142
+ meta_doc = metadata.get_idp_metadata(@settings)
143
+
144
+ # first try POST
145
+ sso_element = REXML::XPath.first(meta_doc,
146
+ "/EntityDescriptor/IDPSSODescriptor/SingleSignOnService[@Binding='#{HTTP_POST}']")
147
+ if sso_element
148
+ @URL = sso_element.attributes["Location"]
149
+ #Logging.debug "binding_select: POST to #{@URL}"
150
+ return "POST", content_post
151
+ end
152
+
153
+ # next try GET
154
+ sso_element = REXML::XPath.first(meta_doc,
155
+ "/EntityDescriptor/IDPSSODescriptor/SingleSignOnService[@Binding='#{HTTP_GET}']")
156
+ if sso_element
157
+ @URL = sso_element.attributes["Location"]
158
+ Logging.debug "binding_select: GET from #{@URL}"
159
+ return "GET", content_get
160
+ end
161
+ # other types we might want to add in the future: SOAP, Artifact
162
+ end
163
+
164
+ # construct the the parameter list on the URL and return
165
+ def content_get
166
+ # compress GET requests to try and stay under that 8KB request limit
167
+ deflated_request = Zlib::Deflate.deflate(@request, 9)[2..-5]
168
+ # strict_encode64() isn't available? sub out the newlines
169
+ @request_params["SAMLRequest"] = Base64.encode64(deflated_request).gsub(/\n/, "")
170
+
171
+ Logging.debug "SAMLRequest=#{@request_params["SAMLRequest"]}"
172
+ uri = Addressable::URI.parse(@URL)
173
+ if uri.query_values == nil
174
+ uri.query_values = @request_params
175
+ else
176
+ # solution to stevenwilkin's parameter merge
177
+ uri.query_values = @request_params.merge(uri.query_values)
178
+ end
179
+ url = uri.to_s
180
+ #Logging.debug "Sending to URL #{url}"
181
+ return url
182
+ end
183
+ # construct an HTML form (POST) and return the content
184
+ def content_post
185
+ # POST requests seem to bomb out when they're deflated
186
+ # and they probably don't need to be compressed anyway
187
+ @request_params["SAMLRequest"] = Base64.encode64(@request).gsub(/\n/, "")
188
+
189
+ #Logging.debug "SAMLRequest=#{@request_params["SAMLRequest"]}"
190
+ # kind of a cheesy method of building an HTML, form since we can't rely on Rails too much,
191
+ # and REXML doesn't work well with quote characters
192
+ str = "<html><body onLoad=\"document.getElementById('form').submit();\">\n"
193
+ str += "<form id='form' name='form' method='POST' action=\"#{@URL}\">\n"
194
+ # we could change this in the future to associate a temp auth session ID
195
+ str += "<input name='RelayState' value='ruby-saml' type='hidden' />\n"
196
+ @request_params.each_pair do |key, value|
197
+ str += "<input name=\"#{key}\" value=\"#{value}\" type='hidden' />\n"
198
+ #str += "<input name=\"#{key}\" value=\"#{CGI.escape(value)}\" type='hidden' />\n"
199
+ end
200
+ str += "</form></body></html>\n"
201
+
202
+ #Logging.debug "Created form:\n#{str}"
203
+ return str
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,34 @@
1
+ require "cgi"
2
+ require 'zlib'
3
+
4
+ module Ciam
5
+ module Saml
6
+ module Coding
7
+ def decode(encoded)
8
+ Base64.decode64(encoded)
9
+ end
10
+
11
+ def encode(encoded)
12
+ Base64.encode64(encoded).gsub(/\n/, "")
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 "ciam/ruby-saml/validation_error"
2
+
3
+ module Ciam
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