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