ruby-saml-federa 0.0.2
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.
- data/.document +5 -0
- data/.gitignore +10 -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/federa/ruby-saml/authrequest.rb +181 -0
- data/lib/federa/ruby-saml/coding.rb +34 -0
- data/lib/federa/ruby-saml/logging.rb +26 -0
- data/lib/federa/ruby-saml/logout_request.rb +126 -0
- data/lib/federa/ruby-saml/logout_response.rb +132 -0
- data/lib/federa/ruby-saml/metadata.rb +266 -0
- data/lib/federa/ruby-saml/request.rb +81 -0
- data/lib/federa/ruby-saml/response.rb +203 -0
- data/lib/federa/ruby-saml/settings.rb +28 -0
- data/lib/federa/ruby-saml/validation_error.rb +7 -0
- data/lib/federa/ruby-saml/version.rb +5 -0
- data/lib/ruby-saml-federa.rb +11 -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/xml_security.rb +165 -0
- data/ruby-saml-federa.gemspec +21 -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 +66 -0
- data/test/xml_security_test.rb +123 -0
- metadata +155 -0
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'uuid'
|
2
|
+
|
3
|
+
module Federa::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 = Federa::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 Federa
|
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,266 @@
|
|
1
|
+
require "rexml/document"
|
2
|
+
require "rexml/xpath"
|
3
|
+
require "net/https"
|
4
|
+
require "uri"
|
5
|
+
require "digest/md5"
|
6
|
+
|
7
|
+
# Class to return SP metadata based on the settings requested.
|
8
|
+
# Return this XML in a controller, then give that URL to the the
|
9
|
+
# IdP administrator. The IdP will poll the URL and your settings
|
10
|
+
# will be updated automatically
|
11
|
+
module Federa
|
12
|
+
module Saml
|
13
|
+
class Metadata
|
14
|
+
include REXML
|
15
|
+
include Coding
|
16
|
+
# a few symbols for SAML class names
|
17
|
+
HTTP_POST = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
|
18
|
+
HTTP_GET = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
|
19
|
+
|
20
|
+
def initialize(settings=nil)
|
21
|
+
if settings
|
22
|
+
@settings = settings
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def generate(settings)
|
27
|
+
meta_doc = REXML::Document.new
|
28
|
+
root = meta_doc.add_element "md:EntityDescriptor", {
|
29
|
+
"xmlns:md" => "urn:oasis:names:tc:SAML:2.0:metadata",
|
30
|
+
"xmlns:xml" => "http://www.w3.org/XML/1998/namespace",
|
31
|
+
"cacheDuration" => "P1M"
|
32
|
+
}
|
33
|
+
if settings.issuer != nil
|
34
|
+
root.attributes["entityID"] = settings.issuer
|
35
|
+
end
|
36
|
+
sp_sso = root.add_element "md:SPSSODescriptor", {
|
37
|
+
"protocolSupportEnumeration" => "urn:oasis:names:tc:SAML:2.0:protocol",
|
38
|
+
"WantAssertionsSigned" => "true"
|
39
|
+
|
40
|
+
}
|
41
|
+
name_identifier_formats = settings.name_identifier_format
|
42
|
+
if name_identifier_formats != nil
|
43
|
+
name_id = []
|
44
|
+
name_identifier_formats.each_with_index{ |format, index|
|
45
|
+
name_id[index] = sp_sso.add_element "md:NameIDFormat"
|
46
|
+
name_id[index].text = format
|
47
|
+
}
|
48
|
+
|
49
|
+
end
|
50
|
+
if settings.sp_cert != nil
|
51
|
+
keyDescriptor = sp_sso.add_element "md:KeyDescriptor", {
|
52
|
+
"use" => "signing"
|
53
|
+
}
|
54
|
+
keyInfo = keyDescriptor.add_element "ds:KeyInfo", {
|
55
|
+
"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"
|
56
|
+
}
|
57
|
+
x509Data = keyInfo.add_element "ds:X509Data"
|
58
|
+
x509Certificate = x509Data.add_element "ds:X509Certificate"
|
59
|
+
file = ""
|
60
|
+
File.foreach(settings.sp_cert){ |line|
|
61
|
+
file += line unless (line.include?("RSA PUBLIC KEY") || line.include?("CERTIFICATE"))
|
62
|
+
}
|
63
|
+
x509Certificate.text = file
|
64
|
+
end
|
65
|
+
if settings.assertion_consumer_service_url != nil
|
66
|
+
sp_sso.add_element "md:AssertionConsumerService", {
|
67
|
+
# Add this as a setting to create different bindings?
|
68
|
+
"Binding" => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
69
|
+
"Location" => settings.assertion_consumer_service_url,
|
70
|
+
"index" => "1"
|
71
|
+
}
|
72
|
+
end
|
73
|
+
if settings.single_logout_service_url != nil
|
74
|
+
sp_sso.add_element "md:SingleLogoutService", {
|
75
|
+
"Binding" => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
|
76
|
+
"Location" => settings.single_logout_service_url
|
77
|
+
}
|
78
|
+
sp_sso.add_element "md:SingleLogoutService", {
|
79
|
+
"Binding" => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
80
|
+
"Location" => settings.single_logout_service_url
|
81
|
+
}
|
82
|
+
end
|
83
|
+
meta_doc << REXML::XMLDecl.new(version='1.0', encoding='UTF-8')
|
84
|
+
ret = ""
|
85
|
+
# pretty print the XML so IdP administrators can easily see what the SP supports
|
86
|
+
meta_doc.write(ret, 1)
|
87
|
+
|
88
|
+
#Logging.debug "Generated metadata:\n#{ret}"
|
89
|
+
|
90
|
+
return ret
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
def create_sso_request(message, extra_parameters = {} )
|
95
|
+
build_message( :type => "SAMLRequest",
|
96
|
+
:service => "SingleSignOnService",
|
97
|
+
:message => message, :extra_parameters => extra_parameters)
|
98
|
+
end
|
99
|
+
def create_sso_response(message, extra_parameters = {} )
|
100
|
+
build_message( :type => "SAMLResponse",
|
101
|
+
:service => "SingleSignOnService",
|
102
|
+
:message => message, :extra_parameters => extra_parameters)
|
103
|
+
end
|
104
|
+
def create_slo_request(message, extra_parameters = {} )
|
105
|
+
build_message( :type => "SAMLRequest",
|
106
|
+
:service => "SingleLogoutService",
|
107
|
+
:message => message, :extra_parameters => extra_parameters)
|
108
|
+
end
|
109
|
+
def create_slo_response(message, extra_parameters = {} )
|
110
|
+
build_message( :type => "SAMLResponse",
|
111
|
+
:service => "SingleLogoutService",
|
112
|
+
:message => message, :extra_parameters => extra_parameters)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Construct a SAML message using information in the IdP metadata.
|
116
|
+
# :type can be either "SAMLRequest" or "SAMLResponse"
|
117
|
+
# :service refers to the Binding method,
|
118
|
+
# either "SingleLogoutService" or "SingleSignOnService"
|
119
|
+
# :message is the SAML message itself (XML)
|
120
|
+
# I've provided easy to use wrapper functions above
|
121
|
+
def build_message( options = {} )
|
122
|
+
opt = { :type => nil, :service => nil, :message => nil, :extra_parameters => nil }.merge(options)
|
123
|
+
url = binding_select( opt[:service] )
|
124
|
+
return message_get( opt[:type], url, opt[:message], opt[:extra_parameters] )
|
125
|
+
end
|
126
|
+
|
127
|
+
# get the IdP metadata, and select the appropriate SSO binding
|
128
|
+
# that we can support. Currently this is HTTP-Redirect and HTTP-POST
|
129
|
+
# but more could be added in the future
|
130
|
+
def binding_select(service)
|
131
|
+
# first check if we're still using the old hard coded method for
|
132
|
+
# backwards compatability
|
133
|
+
if service == "SingleSignOnService" && @settings.idp_metadata == nil && @settings.idp_sso_target_url != nil
|
134
|
+
return @settings.idp_sso_target_url
|
135
|
+
end
|
136
|
+
if service == "SingleLogoutService" && @settings.idp_metadata == nil && @settings.idp_slo_target_url != nil
|
137
|
+
return @settings.idp_slo_target_url
|
138
|
+
end
|
139
|
+
|
140
|
+
meta_doc = get_idp_metadata
|
141
|
+
|
142
|
+
return nil unless meta_doc
|
143
|
+
# first try POST
|
144
|
+
sso_element = REXML::XPath.first(meta_doc, "/md:EntityDescriptor/md:IDPSSODescriptor/md:#{service}[@Binding='#{HTTP_POST}']")
|
145
|
+
if !sso_element.nil?
|
146
|
+
@URL = sso_element.attributes["Location"]
|
147
|
+
#Logging.debug "binding_select: POST to #{@URL}"
|
148
|
+
return @URL
|
149
|
+
end
|
150
|
+
|
151
|
+
# next try GET
|
152
|
+
sso_element = REXML::XPath.first(meta_doc, "/md:EntityDescriptor/md:IDPSSODescriptor/md:#{service}[@Binding='#{HTTP_GET}']")
|
153
|
+
if !sso_element.nil?
|
154
|
+
@URL = sso_element.attributes["Location"]
|
155
|
+
Logging.debug "binding_select: GET from #{@URL}"
|
156
|
+
return @URL
|
157
|
+
end
|
158
|
+
# other types we might want to add in the future: SOAP, Artifact
|
159
|
+
end
|
160
|
+
|
161
|
+
# Retrieve the remote IdP metadata from the URL or a cached copy
|
162
|
+
# returns a REXML document of the metadata
|
163
|
+
def get_idp_metadata
|
164
|
+
return false if @settings.idp_metadata.nil?
|
165
|
+
|
166
|
+
# Look up the metdata in cache first
|
167
|
+
id = Digest::MD5.hexdigest(@settings.idp_metadata)
|
168
|
+
|
169
|
+
uri = URI.parse(@settings.idp_metadata)
|
170
|
+
if uri.scheme == "http"
|
171
|
+
response = Net::HTTP.get_response(uri)
|
172
|
+
meta_text = response.body
|
173
|
+
elsif uri.scheme == "https"
|
174
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
175
|
+
http.use_ssl = true
|
176
|
+
# Most IdPs will probably use self signed certs
|
177
|
+
#http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
178
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
179
|
+
get = Net::HTTP::Get.new(uri.request_uri)
|
180
|
+
response = http.request(get)
|
181
|
+
meta_text = response.body
|
182
|
+
end
|
183
|
+
testo_response = meta_text.sub!(' xmlns:xml="http://www.w3.org/XML/1998/namespace"', '')
|
184
|
+
doc = REXML::Document.new(testo_response)
|
185
|
+
extract_certificate(doc)
|
186
|
+
return doc
|
187
|
+
|
188
|
+
# USE OF CACHE WITH CERTIFICATE
|
189
|
+
# lookup = @cache.read(id)
|
190
|
+
# if lookup != nil
|
191
|
+
# Logging.debug "IdP metadata cached lookup for #{@settings.idp_metadata}"
|
192
|
+
# doc = REXML::Document.new( lookup )
|
193
|
+
# extract_certificate( doc )
|
194
|
+
# return doc
|
195
|
+
# end
|
196
|
+
|
197
|
+
# Logging.debug "IdP metadata cache miss on #{@settings.idp_metadata}"
|
198
|
+
# # cache miss
|
199
|
+
# if File.exists?(@settings.idp_metadata)
|
200
|
+
# fp = File.open( @settings.idp_metadata, "r")
|
201
|
+
# meta_text = fp.read
|
202
|
+
# else
|
203
|
+
# uri = URI.parse(@settings.idp_metadata)
|
204
|
+
# if uri.scheme == "http"
|
205
|
+
# response = Net::HTTP.get_response(uri)
|
206
|
+
# meta_text = response.body
|
207
|
+
# elsif uri.scheme == "https"
|
208
|
+
# http = Net::HTTP.new(uri.host, uri.port)
|
209
|
+
# http.use_ssl = true
|
210
|
+
# # Most IdPs will probably use self signed certs
|
211
|
+
# #http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
212
|
+
# http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
213
|
+
# get = Net::HTTP::Get.new(uri.request_uri)
|
214
|
+
# response = http.request(get)
|
215
|
+
# meta_text = response.body
|
216
|
+
# end
|
217
|
+
# end
|
218
|
+
# # Add it to the cache
|
219
|
+
# @cache.write(id, meta_text, @settings.idp_metadata_ttl )
|
220
|
+
# doc = REXML::Document.new( meta_text )
|
221
|
+
# extract_certificate(doc)
|
222
|
+
# return doc
|
223
|
+
end
|
224
|
+
|
225
|
+
def extract_certificate(meta_doc)
|
226
|
+
|
227
|
+
# pull out the x509 tag
|
228
|
+
x509 = REXML::XPath.first(meta_doc, "/md:EntityDescriptor/md:IDPSSODescriptor"+"/md:KeyDescriptor"+"/ds:KeyInfo/ds:X509Data/ds:X509Certificate"
|
229
|
+
)
|
230
|
+
# If the IdP didn't specify the use attribute
|
231
|
+
if x509.nil?
|
232
|
+
x509 = REXML::XPath.first(meta_doc,
|
233
|
+
"/EntityDescriptor/IDPSSODescriptor" +
|
234
|
+
"/KeyDescriptor" +
|
235
|
+
"/ds:KeyInfo/ds:X509Data/ds:X509Certificate"
|
236
|
+
)
|
237
|
+
end
|
238
|
+
@settings.idp_cert = x509.text.gsub(/\n/, "").gsub(/\t/, "")
|
239
|
+
end
|
240
|
+
|
241
|
+
# construct the parameter list on the URL and return
|
242
|
+
def message_get( type, url, message, extra_parameters = {} )
|
243
|
+
params = Hash.new
|
244
|
+
if extra_parameters
|
245
|
+
params.merge!(extra_parameters)
|
246
|
+
end
|
247
|
+
# compress GET requests to try and stay under that 8KB request limit
|
248
|
+
#deflate of samlrequest
|
249
|
+
params[type] = encode( deflate( message ) )
|
250
|
+
#Logging.debug "#{type}=#{params[type]}"
|
251
|
+
|
252
|
+
uri = Addressable::URI.parse(url)
|
253
|
+
if uri.query_values == nil
|
254
|
+
uri.query_values = params
|
255
|
+
else
|
256
|
+
# solution to stevenwilkin's parameter merge
|
257
|
+
uri.query_values = params.merge(uri.query_values)
|
258
|
+
end
|
259
|
+
url = uri.to_s
|
260
|
+
#Logging.debug "Sending to URL #{url}"
|
261
|
+
return url
|
262
|
+
end
|
263
|
+
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|