spid-es 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|