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