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.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/Gemfile +4 -0
- data/LICENSE +19 -0
- data/README.md +126 -0
- data/Rakefile +41 -0
- data/cie-es.gemspec +22 -0
- data/lib/cie/ruby-saml/authrequest.rb +205 -0
- data/lib/cie/ruby-saml/coding.rb +34 -0
- data/lib/cie/ruby-saml/error_handling.rb +27 -0
- data/lib/cie/ruby-saml/logging.rb +26 -0
- data/lib/cie/ruby-saml/logout_request.rb +126 -0
- data/lib/cie/ruby-saml/logout_response.rb +132 -0
- data/lib/cie/ruby-saml/metadata.rb +489 -0
- data/lib/cie/ruby-saml/request.rb +81 -0
- data/lib/cie/ruby-saml/response.rb +678 -0
- data/lib/cie/ruby-saml/settings.rb +89 -0
- data/lib/cie/ruby-saml/utils.rb +225 -0
- data/lib/cie/ruby-saml/validation_error.rb +7 -0
- data/lib/cie/ruby-saml/version.rb +5 -0
- data/lib/cie/xml_security.rb +166 -0
- data/lib/cie/xml_security_new.rb +373 -0
- data/lib/cie-es.rb +14 -0
- data/lib/schemas/saml20assertion_schema.xsd +283 -0
- data/lib/schemas/saml20protocol_schema.xsd +302 -0
- data/lib/schemas/xenc_schema.xsd +146 -0
- data/lib/schemas/xmldsig_schema.xsd +318 -0
- data/test/certificates/certificate1 +12 -0
- data/test/logoutrequest_test.rb +98 -0
- data/test/request_test.rb +53 -0
- data/test/response_test.rb +219 -0
- data/test/responses/adfs_response_sha1.xml +46 -0
- data/test/responses/adfs_response_sha256.xml +46 -0
- data/test/responses/adfs_response_sha384.xml +46 -0
- data/test/responses/adfs_response_sha512.xml +46 -0
- data/test/responses/no_signature_ns.xml +48 -0
- data/test/responses/open_saml_response.xml +56 -0
- data/test/responses/response1.xml.base64 +1 -0
- data/test/responses/response2.xml.base64 +79 -0
- data/test/responses/response3.xml.base64 +66 -0
- data/test/responses/response4.xml.base64 +93 -0
- data/test/responses/response5.xml.base64 +102 -0
- data/test/responses/response_with_ampersands.xml +139 -0
- data/test/responses/response_with_ampersands.xml.base64 +93 -0
- data/test/responses/simple_saml_php.xml +71 -0
- data/test/responses/wrapped_response_2.xml.base64 +150 -0
- data/test/settings_test.rb +43 -0
- data/test/test_helper.rb +65 -0
- data/test/xml_security_test.rb +123 -0
- metadata +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
data/Gemfile
ADDED
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
|