ruby-saml 0.8.12
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.
Potentially problematic release.
This version of ruby-saml might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/.gitignore +12 -0
- data/.travis.yml +11 -0
- data/Gemfile +37 -0
- data/LICENSE +19 -0
- data/README.md +160 -0
- data/Rakefile +27 -0
- data/changelog.md +24 -0
- data/lib/onelogin/ruby-saml/attributes.rb +147 -0
- data/lib/onelogin/ruby-saml/authrequest.rb +168 -0
- data/lib/onelogin/ruby-saml/logging.rb +26 -0
- data/lib/onelogin/ruby-saml/logoutrequest.rb +161 -0
- data/lib/onelogin/ruby-saml/logoutresponse.rb +153 -0
- data/lib/onelogin/ruby-saml/metadata.rb +66 -0
- data/lib/onelogin/ruby-saml/response.rb +426 -0
- data/lib/onelogin/ruby-saml/setting_error.rb +6 -0
- data/lib/onelogin/ruby-saml/settings.rb +166 -0
- data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +158 -0
- data/lib/onelogin/ruby-saml/utils.rb +119 -0
- data/lib/onelogin/ruby-saml/validation_error.rb +7 -0
- data/lib/onelogin/ruby-saml/version.rb +5 -0
- data/lib/ruby-saml.rb +12 -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 +292 -0
- data/ruby-saml.gemspec +28 -0
- data/test/certificates/certificate1 +12 -0
- data/test/certificates/r1_certificate2_base64 +1 -0
- data/test/certificates/ruby-saml.crt +14 -0
- data/test/certificates/ruby-saml.key +15 -0
- data/test/logoutrequest_test.rb +244 -0
- data/test/logoutresponse_test.rb +112 -0
- data/test/request_test.rb +229 -0
- data/test/response_test.rb +475 -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/encrypted_new_attack.xml.base64 +1 -0
- data/test/responses/logoutresponse_fixtures.rb +67 -0
- data/test/responses/no_signature_ns.xml +48 -0
- data/test/responses/open_saml_response.xml +56 -0
- data/test/responses/r1_response6.xml.base64 +1 -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_eval.xml +7 -0
- data/test/responses/response_node_text_attack.xml.base64 +1 -0
- data/test/responses/response_with_ampersands.xml +139 -0
- data/test/responses/response_with_ampersands.xml.base64 +93 -0
- data/test/responses/response_with_concealed_signed_assertion.xml +51 -0
- data/test/responses/response_with_doubled_signed_assertion.xml +49 -0
- data/test/responses/response_with_multiple_attribute_statements.xml +72 -0
- data/test/responses/response_with_multiple_attribute_values.xml +67 -0
- data/test/responses/response_wrapped.xml.base64 +150 -0
- data/test/responses/simple_saml_php.xml +71 -0
- data/test/responses/starfield_response.xml.base64 +1 -0
- data/test/responses/valid_response.xml.base64 +1 -0
- data/test/responses/wrapped_response_2.xml.base64 +150 -0
- data/test/settings_test.rb +47 -0
- data/test/slo_logoutresponse_test.rb +226 -0
- data/test/test_helper.rb +155 -0
- data/test/utils_test.rb +41 -0
- data/test/xml_security_test.rb +158 -0
- metadata +178 -0
@@ -0,0 +1,168 @@
|
|
1
|
+
require "base64"
|
2
|
+
require "zlib"
|
3
|
+
require "cgi"
|
4
|
+
require "onelogin/ruby-saml/utils"
|
5
|
+
require "onelogin/ruby-saml/setting_error"
|
6
|
+
|
7
|
+
module OneLogin
|
8
|
+
module RubySaml
|
9
|
+
|
10
|
+
class Authrequest
|
11
|
+
# AuthNRequest ID
|
12
|
+
attr_reader :uuid
|
13
|
+
|
14
|
+
# Initializes the AuthNRequest. An Authrequest Object.
|
15
|
+
# Asigns an ID, a random uuid.
|
16
|
+
#
|
17
|
+
def initialize
|
18
|
+
@uuid = OneLogin::RubySaml::Utils.uuid
|
19
|
+
end
|
20
|
+
|
21
|
+
def create(settings, params = {})
|
22
|
+
params = create_params(settings, params)
|
23
|
+
params_prefix = (settings.idp_sso_target_url =~ /\?/) ? '&' : '?'
|
24
|
+
saml_request = CGI.escape(params.delete("SAMLRequest"))
|
25
|
+
request_params = "#{params_prefix}SAMLRequest=#{saml_request}"
|
26
|
+
params.each_pair do |key, value|
|
27
|
+
request_params << "&#{key.to_s}=#{CGI.escape(value.to_s)}"
|
28
|
+
end
|
29
|
+
raise SettingError.new "Invalid settings, idp_sso_target_url is not set!" if settings.idp_sso_target_url.nil? or settings.idp_sso_target_url.empty?
|
30
|
+
@login_url = settings.idp_sso_target_url + request_params
|
31
|
+
end
|
32
|
+
|
33
|
+
# Creates the Get parameters for the request.
|
34
|
+
# @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
|
35
|
+
# @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
|
36
|
+
# @return [Hash] Parameters
|
37
|
+
#
|
38
|
+
def create_params(settings, params={})
|
39
|
+
# The method expects :RelayState but sometimes we get 'RelayState' instead.
|
40
|
+
# Based on the HashWithIndifferentAccess value in Rails we could experience
|
41
|
+
# conflicts so this line will solve them.
|
42
|
+
relay_state = params[:RelayState] || params['RelayState']
|
43
|
+
|
44
|
+
if relay_state.nil?
|
45
|
+
params.delete(:RelayState)
|
46
|
+
params.delete('RelayState')
|
47
|
+
end
|
48
|
+
|
49
|
+
request_doc = create_authentication_xml_doc(settings)
|
50
|
+
request_doc.context[:attribute_quote] = :quote if settings.double_quote_xml_attribute_values
|
51
|
+
|
52
|
+
request = ""
|
53
|
+
request_doc.write(request)
|
54
|
+
|
55
|
+
Logging.debug "Created AuthnRequest: #{request}"
|
56
|
+
|
57
|
+
request = Zlib::Deflate.deflate(request, 9)[2..-5] if settings.compress_request
|
58
|
+
if Base64.respond_to?('strict_encode64')
|
59
|
+
base64_request = Base64.strict_encode64(request)
|
60
|
+
else
|
61
|
+
base64_request = Base64.encode64(request).gsub(/\n/, "")
|
62
|
+
end
|
63
|
+
|
64
|
+
request_params = {"SAMLRequest" => base64_request}
|
65
|
+
|
66
|
+
if settings.security[:authn_requests_signed] && !settings.security[:embed_sign] && settings.private_key
|
67
|
+
params['SigAlg'] = settings.security[:signature_method]
|
68
|
+
url_string = OneLogin::RubySaml::Utils.build_query(
|
69
|
+
:type => 'SAMLRequest',
|
70
|
+
:data => base64_request,
|
71
|
+
:relay_state => relay_state,
|
72
|
+
:sig_alg => params['SigAlg']
|
73
|
+
)
|
74
|
+
sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method])
|
75
|
+
signature = settings.get_sp_key.sign(sign_algorithm.new, url_string)
|
76
|
+
if Base64.respond_to?('strict_encode64')
|
77
|
+
params['Signature'] = Base64.strict_encode64(signature)
|
78
|
+
else
|
79
|
+
params['Signature'] = Base64.encode64(signature).gsub(/\n/, "")
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
params.each_pair do |key, value|
|
84
|
+
request_params[key] = value.to_s
|
85
|
+
end
|
86
|
+
|
87
|
+
request_params
|
88
|
+
end
|
89
|
+
|
90
|
+
def create_authentication_xml_doc(settings)
|
91
|
+
document = create_xml_document(settings)
|
92
|
+
sign_document(document, settings)
|
93
|
+
end
|
94
|
+
|
95
|
+
def create_xml_document(settings)
|
96
|
+
time = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
|
97
|
+
|
98
|
+
request_doc = XMLSecurity::Document.new
|
99
|
+
request_doc.uuid = uuid
|
100
|
+
|
101
|
+
root = request_doc.add_element "samlp:AuthnRequest", { "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol", "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion" }
|
102
|
+
root.attributes['ID'] = uuid
|
103
|
+
root.attributes['IssueInstant'] = time
|
104
|
+
root.attributes['Version'] = "2.0"
|
105
|
+
root.attributes['Destination'] = settings.idp_sso_target_url unless settings.idp_sso_target_url.nil? or settings.idp_sso_target_url.empty?
|
106
|
+
root.attributes['IsPassive'] = settings.passive unless settings.passive.nil?
|
107
|
+
root.attributes['ProtocolBinding'] = settings.protocol_binding unless settings.protocol_binding.nil?
|
108
|
+
root.attributes['ForceAuthn'] = settings.force_authn unless settings.force_authn.nil?
|
109
|
+
|
110
|
+
# Conditionally defined elements based on settings
|
111
|
+
if settings.assertion_consumer_service_url != nil
|
112
|
+
root.attributes["AssertionConsumerServiceURL"] = settings.assertion_consumer_service_url
|
113
|
+
end
|
114
|
+
if settings.sp_entity_id != nil
|
115
|
+
issuer = root.add_element "saml:Issuer"
|
116
|
+
issuer.text = settings.sp_entity_id
|
117
|
+
end
|
118
|
+
|
119
|
+
if settings.name_identifier_value_requested != nil
|
120
|
+
subject = root.add_element "saml:Subject"
|
121
|
+
|
122
|
+
nameid = subject.add_element "saml:NameID"
|
123
|
+
nameid.attributes['Format'] = settings.name_identifier_format if settings.name_identifier_format
|
124
|
+
nameid.text = settings.name_identifier_value_requested
|
125
|
+
|
126
|
+
subject_confirmation = subject.add_element "saml:SubjectConfirmation"
|
127
|
+
subject_confirmation.attributes['Method'] = "urn:oasis:names:tc:SAML:2.0:cm:bearer"
|
128
|
+
end
|
129
|
+
|
130
|
+
if settings.name_identifier_format != nil
|
131
|
+
root.add_element "samlp:NameIDPolicy", {
|
132
|
+
"xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol",
|
133
|
+
# Might want to make AllowCreate a setting?
|
134
|
+
"AllowCreate" => "true",
|
135
|
+
"Format" => settings.name_identifier_format
|
136
|
+
}
|
137
|
+
end
|
138
|
+
|
139
|
+
# BUG fix here -- if an authn_context is defined, add the tags with an "exact"
|
140
|
+
# match required for authentication to succeed. If this is not defined,
|
141
|
+
# the IdP will choose default rules for authentication. (Shibboleth IdP)
|
142
|
+
if settings.authn_context != nil
|
143
|
+
requested_context = root.add_element "samlp:RequestedAuthnContext", {
|
144
|
+
"xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol",
|
145
|
+
"Comparison" => "exact",
|
146
|
+
}
|
147
|
+
class_ref = requested_context.add_element "saml:AuthnContextClassRef", {
|
148
|
+
"xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion",
|
149
|
+
}
|
150
|
+
class_ref.text = settings.authn_context
|
151
|
+
end
|
152
|
+
request_doc
|
153
|
+
end
|
154
|
+
|
155
|
+
def sign_document(document, settings)
|
156
|
+
# embed signature
|
157
|
+
if settings.security[:authn_requests_signed] && settings.private_key && settings.certificate && settings.security[:embed_sign]
|
158
|
+
private_key = settings.get_sp_key
|
159
|
+
cert = settings.get_sp_cert
|
160
|
+
document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
|
161
|
+
end
|
162
|
+
|
163
|
+
document
|
164
|
+
end
|
165
|
+
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# Simplistic log class when we're running in Rails
|
2
|
+
module OneLogin
|
3
|
+
module RubySaml
|
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,161 @@
|
|
1
|
+
require "base64"
|
2
|
+
require "zlib"
|
3
|
+
require "cgi"
|
4
|
+
require 'rexml/document'
|
5
|
+
require "onelogin/ruby-saml/utils"
|
6
|
+
require "onelogin/ruby-saml/setting_error"
|
7
|
+
|
8
|
+
module OneLogin
|
9
|
+
module RubySaml
|
10
|
+
|
11
|
+
class Logoutrequest
|
12
|
+
|
13
|
+
attr_reader :uuid # Can be obtained if neccessary
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@uuid = OneLogin::RubySaml::Utils.uuid
|
17
|
+
end
|
18
|
+
|
19
|
+
def create(settings, params={})
|
20
|
+
params = create_params(settings, params)
|
21
|
+
params_prefix = (settings.idp_slo_target_url =~ /\?/) ? '&' : '?'
|
22
|
+
saml_request = CGI.escape(params.delete("SAMLRequest"))
|
23
|
+
request_params = "#{params_prefix}SAMLRequest=#{saml_request}"
|
24
|
+
params.each_pair do |key, value|
|
25
|
+
request_params << "&#{key.to_s}=#{CGI.escape(value.to_s)}"
|
26
|
+
end
|
27
|
+
raise SettingError.new "Invalid settings, idp_slo_target_url is not set!" if settings.idp_slo_target_url.nil? or settings.idp_slo_target_url.empty?
|
28
|
+
@logout_url = settings.idp_slo_target_url + request_params
|
29
|
+
end
|
30
|
+
|
31
|
+
# Creates the Get parameters for the logout request.
|
32
|
+
# @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
|
33
|
+
# @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
|
34
|
+
# @return [Hash] Parameters
|
35
|
+
#
|
36
|
+
def create_params(settings, params={})
|
37
|
+
# The method expects :RelayState but sometimes we get 'RelayState' instead.
|
38
|
+
# Based on the HashWithIndifferentAccess value in Rails we could experience
|
39
|
+
# conflicts so this line will solve them.
|
40
|
+
relay_state = params[:RelayState] || params['RelayState']
|
41
|
+
|
42
|
+
if relay_state.nil?
|
43
|
+
params.delete(:RelayState)
|
44
|
+
params.delete('RelayState')
|
45
|
+
end
|
46
|
+
|
47
|
+
request_doc = create_logout_request_xml_doc(settings)
|
48
|
+
request_doc.context[:attribute_quote] = :quote if settings.double_quote_xml_attribute_values
|
49
|
+
|
50
|
+
request = ""
|
51
|
+
request_doc.write(request)
|
52
|
+
|
53
|
+
Logging.debug "Created SLO Logout Request: #{request}"
|
54
|
+
|
55
|
+
request = Zlib::Deflate.deflate(request, 9)[2..-5] if settings.compress_request
|
56
|
+
if Base64.respond_to?('strict_encode64')
|
57
|
+
base64_request = Base64.strict_encode64(request)
|
58
|
+
else
|
59
|
+
base64_request = Base64.encode64(request).gsub(/\n/, "")
|
60
|
+
end
|
61
|
+
request_params = {"SAMLRequest" => base64_request}
|
62
|
+
|
63
|
+
if settings.security[:logout_requests_signed] && !settings.security[:embed_sign] && settings.private_key
|
64
|
+
params['SigAlg'] = settings.security[:signature_method]
|
65
|
+
url_string = OneLogin::RubySaml::Utils.build_query(
|
66
|
+
:type => 'SAMLRequest',
|
67
|
+
:data => base64_request,
|
68
|
+
:relay_state => relay_state,
|
69
|
+
:sig_alg => params['SigAlg']
|
70
|
+
)
|
71
|
+
sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method])
|
72
|
+
signature = settings.get_sp_key.sign(sign_algorithm.new, url_string)
|
73
|
+
if Base64.respond_to?('strict_encode64')
|
74
|
+
params['Signature'] = Base64.strict_encode64(signature)
|
75
|
+
else
|
76
|
+
params['Signature'] = Base64.encode64(signature).gsub(/\n/, "")
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
params.each_pair do |key, value|
|
81
|
+
request_params[key] = value.to_s
|
82
|
+
end
|
83
|
+
|
84
|
+
request_params
|
85
|
+
end
|
86
|
+
|
87
|
+
# Creates the SAMLRequest String.
|
88
|
+
# @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
|
89
|
+
# @return [String] The SAMLRequest String.
|
90
|
+
#
|
91
|
+
def create_logout_request_xml_doc(settings)
|
92
|
+
document = create_xml_document(settings)
|
93
|
+
sign_document(document, settings)
|
94
|
+
end
|
95
|
+
|
96
|
+
def create_xml_document(settings, request_doc=nil)
|
97
|
+
time = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
|
98
|
+
|
99
|
+
if request_doc.nil?
|
100
|
+
request_doc = XMLSecurity::Document.new
|
101
|
+
request_doc.uuid = uuid
|
102
|
+
end
|
103
|
+
|
104
|
+
root = request_doc.add_element "samlp:LogoutRequest", { "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol" }
|
105
|
+
root.attributes['ID'] = uuid
|
106
|
+
root.attributes['IssueInstant'] = time
|
107
|
+
root.attributes['Version'] = "2.0"
|
108
|
+
root.attributes['Destination'] = settings.idp_slo_target_url unless settings.idp_slo_target_url.nil? or settings.idp_slo_target_url.empty?
|
109
|
+
|
110
|
+
if settings.sp_entity_id
|
111
|
+
issuer = root.add_element "saml:Issuer", { "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion" }
|
112
|
+
issuer.text = settings.sp_entity_id
|
113
|
+
end
|
114
|
+
|
115
|
+
if settings.name_identifier_value
|
116
|
+
name_id = root.add_element "saml:NameID", { "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion" }
|
117
|
+
name_id.attributes['NameQualifier'] = settings.sp_name_qualifier if settings.sp_name_qualifier
|
118
|
+
name_id.attributes['Format'] = settings.name_identifier_format if settings.name_identifier_format
|
119
|
+
name_id.text = settings.name_identifier_value
|
120
|
+
end
|
121
|
+
|
122
|
+
if settings.sessionindex
|
123
|
+
sessionindex = root.add_element "samlp:SessionIndex", { "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol" }
|
124
|
+
sessionindex.text = settings.sessionindex
|
125
|
+
end
|
126
|
+
|
127
|
+
# BUG fix here -- if an authn_context is defined, add the tags with an "exact"
|
128
|
+
# match required for authentication to succeed. If this is not defined,
|
129
|
+
# the IdP will choose default rules for authentication. (Shibboleth IdP)
|
130
|
+
if settings.authn_context != nil
|
131
|
+
requested_context = root.add_element "samlp:RequestedAuthnContext", {
|
132
|
+
"xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol",
|
133
|
+
"Comparison" => "exact",
|
134
|
+
}
|
135
|
+
class_ref = requested_context.add_element "saml:AuthnContextClassRef", {
|
136
|
+
"xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion",
|
137
|
+
}
|
138
|
+
class_ref.text = settings.authn_context
|
139
|
+
end
|
140
|
+
request_doc
|
141
|
+
end
|
142
|
+
|
143
|
+
def sign_document(document, settings)
|
144
|
+
# embed signature
|
145
|
+
if settings.security[:logout_requests_signed] && settings.private_key && settings.certificate && settings.security[:embed_sign]
|
146
|
+
private_key = settings.get_sp_key
|
147
|
+
cert = settings.get_sp_cert
|
148
|
+
document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
|
149
|
+
end
|
150
|
+
|
151
|
+
document
|
152
|
+
end
|
153
|
+
|
154
|
+
# Leave due compatibility
|
155
|
+
def create_unauth_xml_doc(settings, params)
|
156
|
+
request_doc = ReXML::Document.new
|
157
|
+
create_xml_document(settings, request_doc)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
require "xml_security"
|
2
|
+
require "time"
|
3
|
+
require "base64"
|
4
|
+
require "zlib"
|
5
|
+
|
6
|
+
module OneLogin
|
7
|
+
module RubySaml
|
8
|
+
class Logoutresponse
|
9
|
+
|
10
|
+
ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
|
11
|
+
PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
|
12
|
+
|
13
|
+
# For API compability, this is mutable.
|
14
|
+
attr_accessor :settings
|
15
|
+
|
16
|
+
attr_reader :document
|
17
|
+
attr_reader :response
|
18
|
+
attr_reader :options
|
19
|
+
|
20
|
+
#
|
21
|
+
# In order to validate that the response matches a given request, append
|
22
|
+
# the option:
|
23
|
+
# :matches_request_id => REQUEST_ID
|
24
|
+
#
|
25
|
+
# It will validate that the logout response matches the ID of the request.
|
26
|
+
# You can also do this yourself through the in_response_to accessor.
|
27
|
+
#
|
28
|
+
def initialize(response, settings = nil, options = {})
|
29
|
+
raise ArgumentError.new("Logoutresponse cannot be nil") if response.nil?
|
30
|
+
self.settings = settings
|
31
|
+
|
32
|
+
@options = options
|
33
|
+
@response = decode_raw_response(response)
|
34
|
+
@document = XMLSecurity::SignedDocument.new(response)
|
35
|
+
end
|
36
|
+
|
37
|
+
def validate!
|
38
|
+
validate(false)
|
39
|
+
end
|
40
|
+
|
41
|
+
def validate(soft = true)
|
42
|
+
return false unless valid_saml?(soft) && valid_state?(soft)
|
43
|
+
|
44
|
+
valid_in_response_to?(soft) && valid_issuer?(soft) && success?(soft)
|
45
|
+
end
|
46
|
+
|
47
|
+
def success?(soft = true)
|
48
|
+
unless status_code == "urn:oasis:names:tc:SAML:2.0:status:Success"
|
49
|
+
return soft ? false : validation_error("Bad status code. Expected <urn:oasis:names:tc:SAML:2.0:status:Success>, but was: <#@status_code> ")
|
50
|
+
end
|
51
|
+
true
|
52
|
+
end
|
53
|
+
|
54
|
+
def in_response_to
|
55
|
+
@in_response_to ||= begin
|
56
|
+
node = REXML::XPath.first(document, "/p:LogoutResponse", { "p" => PROTOCOL, "a" => ASSERTION })
|
57
|
+
node.nil? ? nil : node.attributes['InResponseTo']
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def issuer
|
62
|
+
@issuer ||= begin
|
63
|
+
node = REXML::XPath.first(document, "/p:LogoutResponse/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
|
64
|
+
node ||= REXML::XPath.first(document, "/p:LogoutResponse/a:Assertion/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
|
65
|
+
Utils.element_text(node)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def status_code
|
70
|
+
@status_code ||= begin
|
71
|
+
node = REXML::XPath.first(document, "/p:LogoutResponse/p:Status/p:StatusCode", { "p" => PROTOCOL, "a" => ASSERTION })
|
72
|
+
node.nil? ? nil : node.attributes["Value"]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def decode(encoded)
|
79
|
+
Base64.decode64(encoded)
|
80
|
+
end
|
81
|
+
|
82
|
+
def inflate(deflated)
|
83
|
+
zlib = Zlib::Inflate.new(-Zlib::MAX_WBITS)
|
84
|
+
zlib.inflate(deflated)
|
85
|
+
end
|
86
|
+
|
87
|
+
def decode_raw_response(response)
|
88
|
+
if response =~ /^</
|
89
|
+
return response
|
90
|
+
elsif (decoded = decode(response)) =~ /^</
|
91
|
+
return decoded
|
92
|
+
elsif (inflated = inflate(decoded)) =~ /^</
|
93
|
+
return inflated
|
94
|
+
end
|
95
|
+
|
96
|
+
raise "Couldn't decode SAMLResponse"
|
97
|
+
end
|
98
|
+
|
99
|
+
def valid_saml?(soft = true)
|
100
|
+
Dir.chdir(File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'schemas'))) do
|
101
|
+
@schema = Nokogiri::XML::Schema(IO.read('saml20protocol_schema.xsd'))
|
102
|
+
@xml = Nokogiri::XML(self.document.to_s)
|
103
|
+
end
|
104
|
+
if soft
|
105
|
+
@schema.validate(@xml).map{ return false }
|
106
|
+
else
|
107
|
+
@schema.validate(@xml).map{ |error| validation_error("#{error.message}\n\n#{@xml.to_s}") }
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def valid_state?(soft = true)
|
112
|
+
if response.empty?
|
113
|
+
return soft ? false : validation_error("Blank response")
|
114
|
+
end
|
115
|
+
|
116
|
+
if settings.nil?
|
117
|
+
return soft ? false : validation_error("No settings on response")
|
118
|
+
end
|
119
|
+
|
120
|
+
if settings.sp_entity_id.nil?
|
121
|
+
return soft ? false : validation_error("No sp_entity_id in settings")
|
122
|
+
end
|
123
|
+
|
124
|
+
if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil?
|
125
|
+
return soft ? false : validation_error("No fingerprint or certificate on settings")
|
126
|
+
end
|
127
|
+
|
128
|
+
true
|
129
|
+
end
|
130
|
+
|
131
|
+
def valid_in_response_to?(soft = true)
|
132
|
+
return true unless self.options.has_key? :matches_request_id
|
133
|
+
|
134
|
+
unless self.options[:matches_request_id] == in_response_to
|
135
|
+
return soft ? false : validation_error("Response does not match the request ID, expected: <#{self.options[:matches_request_id]}>, but was: <#{in_response_to}>")
|
136
|
+
end
|
137
|
+
|
138
|
+
true
|
139
|
+
end
|
140
|
+
|
141
|
+
def valid_issuer?(soft = true)
|
142
|
+
unless URI.parse(issuer) == URI.parse(self.settings.sp_entity_id)
|
143
|
+
return soft ? false : validation_error("Doesn't match the issuer, expected: <#{self.settings.sp_entity_id}>, but was: <#{issuer}>")
|
144
|
+
end
|
145
|
+
true
|
146
|
+
end
|
147
|
+
|
148
|
+
def validation_error(message)
|
149
|
+
raise ValidationError.new(message)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|