r-saml 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/.gitignore +14 -0
- data/.travis.yml +23 -0
- data/Gemfile +6 -0
- data/LICENSE +19 -0
- data/README.md +584 -0
- data/Rakefile +27 -0
- data/changelog.md +75 -0
- data/gemfiles/nokogiri-1.5.gemfile +5 -0
- data/lib/onelogin/ruby-saml.rb +17 -0
- data/lib/onelogin/ruby-saml/attribute_service.rb +57 -0
- data/lib/onelogin/ruby-saml/attributes.rb +128 -0
- data/lib/onelogin/ruby-saml/authrequest.rb +165 -0
- data/lib/onelogin/ruby-saml/http_error.rb +7 -0
- data/lib/onelogin/ruby-saml/idp_metadata_parser.rb +161 -0
- data/lib/onelogin/ruby-saml/logging.rb +30 -0
- data/lib/onelogin/ruby-saml/logoutrequest.rb +131 -0
- data/lib/onelogin/ruby-saml/logoutresponse.rb +241 -0
- data/lib/onelogin/ruby-saml/metadata.rb +123 -0
- data/lib/onelogin/ruby-saml/response.rb +735 -0
- data/lib/onelogin/ruby-saml/saml_message.rb +158 -0
- data/lib/onelogin/ruby-saml/settings.rb +165 -0
- data/lib/onelogin/ruby-saml/slo_logoutrequest.rb +258 -0
- data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +136 -0
- data/lib/onelogin/ruby-saml/utils.rb +172 -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 +1 -0
- data/lib/schemas/saml-schema-assertion-2.0.xsd +283 -0
- data/lib/schemas/saml-schema-authn-context-2.0.xsd +23 -0
- data/lib/schemas/saml-schema-authn-context-types-2.0.xsd +821 -0
- data/lib/schemas/saml-schema-metadata-2.0.xsd +337 -0
- data/lib/schemas/saml-schema-protocol-2.0.xsd +302 -0
- data/lib/schemas/sstc-metadata-attr.xsd +35 -0
- data/lib/schemas/sstc-saml-attribute-ext.xsd +25 -0
- data/lib/schemas/sstc-saml-metadata-algsupport-v1.0.xsd +41 -0
- data/lib/schemas/sstc-saml-metadata-ui-v1.0.xsd +89 -0
- data/lib/schemas/xenc-schema.xsd +136 -0
- data/lib/schemas/xml.xsd +287 -0
- data/lib/schemas/xmldsig-core-schema.xsd +309 -0
- data/lib/xml_security.rb +368 -0
- data/r-saml.gemspec +64 -0
- data/test/certificates/certificate1 +12 -0
- data/test/certificates/certificate_without_head_foot +1 -0
- data/test/certificates/formatted_certificate +14 -0
- data/test/certificates/formatted_private_key +12 -0
- data/test/certificates/formatted_rsa_private_key +12 -0
- data/test/certificates/invalid_certificate1 +1 -0
- data/test/certificates/invalid_certificate2 +1 -0
- data/test/certificates/invalid_certificate3 +12 -0
- data/test/certificates/invalid_private_key1 +1 -0
- data/test/certificates/invalid_private_key2 +1 -0
- data/test/certificates/invalid_private_key3 +10 -0
- data/test/certificates/invalid_rsa_private_key1 +1 -0
- data/test/certificates/invalid_rsa_private_key2 +1 -0
- data/test/certificates/invalid_rsa_private_key3 +10 -0
- data/test/certificates/ruby-saml.crt +14 -0
- data/test/certificates/ruby-saml.key +15 -0
- data/test/idp_metadata_parser_test.rb +95 -0
- data/test/logging_test.rb +62 -0
- data/test/logout_requests/invalid_slo_request.xml +6 -0
- data/test/logout_requests/slo_request.xml +4 -0
- data/test/logout_requests/slo_request.xml.base64 +1 -0
- data/test/logout_requests/slo_request_deflated.xml.base64 +1 -0
- data/test/logout_requests/slo_request_with_session_index.xml +5 -0
- data/test/logout_responses/logoutresponse_fixtures.rb +67 -0
- data/test/logoutrequest_test.rb +211 -0
- data/test/logoutresponse_test.rb +258 -0
- data/test/metadata_test.rb +203 -0
- data/test/request_test.rb +282 -0
- data/test/response_test.rb +1159 -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/adfs_response_xmlns.xml +45 -0
- data/test/responses/attackxee.xml +13 -0
- data/test/responses/idp_descriptor.xml +3 -0
- data/test/responses/invalids/invalid_audience.xml.base64 +1 -0
- data/test/responses/invalids/invalid_issuer_assertion.xml.base64 +1 -0
- data/test/responses/invalids/invalid_issuer_message.xml.base64 +1 -0
- data/test/responses/invalids/invalid_signature_position.xml.base64 +1 -0
- data/test/responses/invalids/invalid_subjectconfirmation_inresponse.xml.base64 +1 -0
- data/test/responses/invalids/invalid_subjectconfirmation_nb.xml.base64 +1 -0
- data/test/responses/invalids/invalid_subjectconfirmation_noa.xml.base64 +1 -0
- data/test/responses/invalids/invalid_subjectconfirmation_recipient.xml.base64 +1 -0
- data/test/responses/invalids/multiple_assertions.xml.base64 +2 -0
- data/test/responses/invalids/multiple_signed.xml.base64 +1 -0
- data/test/responses/invalids/no_id.xml.base64 +1 -0
- data/test/responses/invalids/no_saml2.xml.base64 +1 -0
- data/test/responses/invalids/no_signature.xml.base64 +1 -0
- data/test/responses/invalids/no_status.xml.base64 +1 -0
- data/test/responses/invalids/no_status_code.xml.base64 +1 -0
- data/test/responses/invalids/no_subjectconfirmation_data.xml.base64 +1 -0
- data/test/responses/invalids/no_subjectconfirmation_method.xml.base64 +1 -0
- data/test/responses/invalids/response_encrypted_attrs.xml.base64 +1 -0
- data/test/responses/invalids/response_invalid_signed_element.xml.base64 +1 -0
- data/test/responses/invalids/status_code_responder.xml.base64 +1 -0
- data/test/responses/invalids/status_code_responer_and_msg.xml.base64 +1 -0
- data/test/responses/no_signature_ns.xml +48 -0
- data/test/responses/open_saml_response.xml +56 -0
- data/test/responses/response_assertion_wrapped.xml.base64 +93 -0
- data/test/responses/response_encrypted_nameid.xml.base64 +1 -0
- data/test/responses/response_eval.xml +7 -0
- data/test/responses/response_no_cert_and_encrypted_attrs.xml +29 -0
- data/test/responses/response_unsigned_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_multiple_attribute_values.xml +67 -0
- data/test/responses/response_with_saml2_namespace.xml.base64 +102 -0
- data/test/responses/response_with_signed_assertion.xml.base64 +66 -0
- data/test/responses/response_with_signed_assertion_2.xml.base64 +1 -0
- data/test/responses/response_with_undefined_recipient.xml.base64 +1 -0
- data/test/responses/response_without_attributes.xml.base64 +79 -0
- data/test/responses/response_without_reference_uri.xml.base64 +1 -0
- data/test/responses/response_wrapped.xml.base64 +150 -0
- data/test/responses/signed_message_encrypted_signed_assertion.xml.base64 +1 -0
- data/test/responses/signed_message_encrypted_unsigned_assertion.xml.base64 +1 -0
- data/test/responses/signed_nameid_in_atts.xml +47 -0
- data/test/responses/signed_unqual_nameid_in_atts.xml +47 -0
- data/test/responses/simple_saml_php.xml +71 -0
- data/test/responses/starfield_response.xml.base64 +1 -0
- data/test/responses/test_sign.xml +43 -0
- data/test/responses/unsigned_message_aes128_encrypted_signed_assertion.xml.base64 +1 -0
- data/test/responses/unsigned_message_aes192_encrypted_signed_assertion.xml.base64 +1 -0
- data/test/responses/unsigned_message_aes256_encrypted_signed_assertion.xml.base64 +1 -0
- data/test/responses/unsigned_message_des192_encrypted_signed_assertion.xml.base64 +1 -0
- data/test/responses/unsigned_message_encrypted_assertion_without_saml_namespace.xml.base64 +1 -0
- data/test/responses/unsigned_message_encrypted_signed_assertion.xml.base64 +1 -0
- data/test/responses/unsigned_message_encrypted_unsigned_assertion.xml.base64 +1 -0
- data/test/responses/valid_response.xml.base64 +1 -0
- data/test/saml_message_test.rb +56 -0
- data/test/settings_test.rb +218 -0
- data/test/slo_logoutrequest_test.rb +275 -0
- data/test/slo_logoutresponse_test.rb +185 -0
- data/test/test_helper.rb +257 -0
- data/test/utils_test.rb +145 -0
- data/test/xml_security_test.rb +328 -0
- metadata +421 -0
@@ -0,0 +1,161 @@
|
|
1
|
+
require "base64"
|
2
|
+
require "uuid"
|
3
|
+
require "zlib"
|
4
|
+
require "cgi"
|
5
|
+
require "net/http"
|
6
|
+
require "net/https"
|
7
|
+
require "rexml/document"
|
8
|
+
require "rexml/xpath"
|
9
|
+
|
10
|
+
# Only supports SAML 2.0
|
11
|
+
module OneLogin
|
12
|
+
module RubySaml
|
13
|
+
include REXML
|
14
|
+
|
15
|
+
# Auxiliary class to retrieve and parse the Identity Provider Metadata
|
16
|
+
#
|
17
|
+
class IdpMetadataParser
|
18
|
+
|
19
|
+
METADATA = "urn:oasis:names:tc:SAML:2.0:metadata"
|
20
|
+
DSIG = "http://www.w3.org/2000/09/xmldsig#"
|
21
|
+
|
22
|
+
attr_reader :document
|
23
|
+
attr_reader :response
|
24
|
+
|
25
|
+
# Parse the Identity Provider metadata and update the settings with the
|
26
|
+
# IdP values
|
27
|
+
#
|
28
|
+
# @param (see IdpMetadataParser#get_idp_metadata)
|
29
|
+
# @return (see IdpMetadataParser#get_idp_metadata)
|
30
|
+
# @raise (see IdpMetadataParser#get_idp_metadata)
|
31
|
+
def parse_remote(url, validate_cert = true)
|
32
|
+
idp_metadata = get_idp_metadata(url, validate_cert)
|
33
|
+
parse(idp_metadata)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Parse the Identity Provider metadata and update the settings with the IdP values
|
37
|
+
# @param idp_metadata [String]
|
38
|
+
#
|
39
|
+
def parse(idp_metadata)
|
40
|
+
@document = REXML::Document.new(idp_metadata)
|
41
|
+
|
42
|
+
OneLogin::RubySaml::Settings.new.tap do |settings|
|
43
|
+
settings.idp_entity_id = idp_entity_id
|
44
|
+
settings.name_identifier_format = idp_name_id_format
|
45
|
+
settings.idp_sso_target_url = single_signon_service_url
|
46
|
+
settings.idp_slo_target_url = single_logout_service_url
|
47
|
+
settings.idp_cert_fingerprint = fingerprint
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
# Retrieve the remote IdP metadata from the URL or a cached copy.
|
54
|
+
# @param url [String] Url where the XML of the Identity Provider Metadata is published.
|
55
|
+
# @param validate_cert [Boolean] If true and the URL is HTTPs, the cert of the domain is checked.
|
56
|
+
# @return [REXML::document] Parsed XML IdP metadata
|
57
|
+
# @raise [HttpError] Failure to fetch remote IdP metadata
|
58
|
+
def get_idp_metadata(url, validate_cert)
|
59
|
+
uri = URI.parse(url)
|
60
|
+
if uri.scheme == "http"
|
61
|
+
response = Net::HTTP.get_response(uri)
|
62
|
+
meta_text = response.body
|
63
|
+
elsif uri.scheme == "https"
|
64
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
65
|
+
http.use_ssl = true
|
66
|
+
# Most IdPs will probably use self signed certs
|
67
|
+
if validate_cert
|
68
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
69
|
+
|
70
|
+
# Net::HTTP in Ruby 1.8 did not set the default certificate store
|
71
|
+
# automatically when VERIFY_PEER was specified.
|
72
|
+
if RUBY_VERSION < '1.9' && !http.ca_file && !http.ca_path && !http.cert_store
|
73
|
+
http.cert_store = OpenSSL::SSL::SSLContext::DEFAULT_CERT_STORE
|
74
|
+
end
|
75
|
+
else
|
76
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
77
|
+
end
|
78
|
+
get = Net::HTTP::Get.new(uri.request_uri)
|
79
|
+
response = http.request(get)
|
80
|
+
meta_text = response.body
|
81
|
+
else
|
82
|
+
raise ArgumentError.new("url must begin with http or https")
|
83
|
+
end
|
84
|
+
|
85
|
+
unless response.is_a? Net::HTTPSuccess
|
86
|
+
raise OneLogin::RubySaml::HttpError.new("Failed to fetch idp metadata")
|
87
|
+
end
|
88
|
+
|
89
|
+
meta_text
|
90
|
+
end
|
91
|
+
|
92
|
+
# @return [String|nil] IdP Entity ID value if exists
|
93
|
+
#
|
94
|
+
def idp_entity_id
|
95
|
+
node = REXML::XPath.first(
|
96
|
+
document,
|
97
|
+
"/md:EntityDescriptor/@entityID",
|
98
|
+
{ "md" => METADATA }
|
99
|
+
)
|
100
|
+
node.value if node
|
101
|
+
end
|
102
|
+
|
103
|
+
# @return [String|nil] IdP Name ID Format value if exists
|
104
|
+
#
|
105
|
+
def idp_name_id_format
|
106
|
+
node = REXML::XPath.first(
|
107
|
+
document,
|
108
|
+
"/md:EntityDescriptor/md:IDPSSODescriptor/md:NameIDFormat",
|
109
|
+
{ "md" => METADATA }
|
110
|
+
)
|
111
|
+
node.text if node
|
112
|
+
end
|
113
|
+
|
114
|
+
# @return [String|nil] SingleSignOnService endpoint if exists
|
115
|
+
#
|
116
|
+
def single_signon_service_url
|
117
|
+
node = REXML::XPath.first(
|
118
|
+
document,
|
119
|
+
"/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleSignOnService/@Location",
|
120
|
+
{ "md" => METADATA }
|
121
|
+
)
|
122
|
+
node.value if node
|
123
|
+
end
|
124
|
+
|
125
|
+
# @return [String|nil] SingleLogoutService endpoint if exists
|
126
|
+
#
|
127
|
+
def single_logout_service_url
|
128
|
+
node = REXML::XPath.first(
|
129
|
+
document,
|
130
|
+
"/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleLogoutService/@Location",
|
131
|
+
{ "md" => METADATA }
|
132
|
+
)
|
133
|
+
node.value if node
|
134
|
+
end
|
135
|
+
|
136
|
+
# @return [String|nil] X509Certificate if exists
|
137
|
+
#
|
138
|
+
def certificate
|
139
|
+
@certificate ||= begin
|
140
|
+
node = REXML::XPath.first(
|
141
|
+
document,
|
142
|
+
"/md:EntityDescriptor/md:IDPSSODescriptor/md:KeyDescriptor[@use='signing']/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
|
143
|
+
{ "md" => METADATA, "ds" => DSIG }
|
144
|
+
)
|
145
|
+
Base64.decode64(node.text) if node
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# @return [String|nil] the SHA-1 fingerpint of the X509Certificate if it exists
|
150
|
+
#
|
151
|
+
def fingerprint
|
152
|
+
@fingerprint ||= begin
|
153
|
+
if certificate
|
154
|
+
cert = OpenSSL::X509::Certificate.new(certificate)
|
155
|
+
Digest::SHA1.hexdigest(cert.to_der).upcase.scan(/../).join(":")
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
# Simplistic log class when we're running in Rails
|
4
|
+
module OneLogin
|
5
|
+
module RubySaml
|
6
|
+
class Logging
|
7
|
+
DEFAULT_LOGGER = ::Logger.new(STDOUT)
|
8
|
+
|
9
|
+
def self.logger
|
10
|
+
@logger || (defined?(::Rails) && Rails.logger) || DEFAULT_LOGGER
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.logger=(logger)
|
14
|
+
@logger = logger
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.debug(message)
|
18
|
+
return if !!ENV["ruby-saml/testing"]
|
19
|
+
|
20
|
+
logger.debug message
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.info(message)
|
24
|
+
return if !!ENV["ruby-saml/testing"]
|
25
|
+
|
26
|
+
logger.info message
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require "uuid"
|
2
|
+
|
3
|
+
require "onelogin/ruby-saml/logging"
|
4
|
+
require "onelogin/ruby-saml/saml_message"
|
5
|
+
|
6
|
+
# Only supports SAML 2.0
|
7
|
+
module OneLogin
|
8
|
+
module RubySaml
|
9
|
+
|
10
|
+
# SAML2 Logout Request (SLO SP initiated, Builder)
|
11
|
+
#
|
12
|
+
class Logoutrequest < SamlMessage
|
13
|
+
|
14
|
+
# Logout Request ID
|
15
|
+
attr_reader :uuid
|
16
|
+
|
17
|
+
# Initializes the Logout Request. A Logoutrequest Object that is an extension of the SamlMessage class.
|
18
|
+
# Asigns an ID, a random uuid.
|
19
|
+
#
|
20
|
+
def initialize
|
21
|
+
@uuid = "_" + UUID.new.generate
|
22
|
+
end
|
23
|
+
|
24
|
+
# Creates the Logout Request string.
|
25
|
+
# @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
|
26
|
+
# @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
|
27
|
+
# @return [String] Logout Request string that includes the SAMLRequest
|
28
|
+
#
|
29
|
+
def create(settings, params={})
|
30
|
+
params = create_params(settings, params)
|
31
|
+
params_prefix = (settings.idp_slo_target_url =~ /\?/) ? '&' : '?'
|
32
|
+
saml_request = CGI.escape(params.delete("SAMLRequest"))
|
33
|
+
request_params = "#{params_prefix}SAMLRequest=#{saml_request}"
|
34
|
+
params.each_pair do |key, value|
|
35
|
+
request_params << "&#{key.to_s}=#{CGI.escape(value.to_s)}"
|
36
|
+
end
|
37
|
+
@logout_url = settings.idp_slo_target_url + request_params
|
38
|
+
end
|
39
|
+
|
40
|
+
# Creates the Get parameters for the logout request.
|
41
|
+
# @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
|
42
|
+
# @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
|
43
|
+
# @return [Hash] Parameters
|
44
|
+
#
|
45
|
+
def create_params(settings, params={})
|
46
|
+
# The method expects :RelayState but sometimes we get 'RelayState' instead.
|
47
|
+
# Based on the HashWithIndifferentAccess value in Rails we could experience
|
48
|
+
# conflicts so this line will solve them.
|
49
|
+
relay_state = params[:RelayState] || params['RelayState']
|
50
|
+
|
51
|
+
request_doc = create_logout_request_xml_doc(settings)
|
52
|
+
request_doc.context[:attribute_quote] = :quote if settings.double_quote_xml_attribute_values
|
53
|
+
|
54
|
+
request = ""
|
55
|
+
request_doc.write(request)
|
56
|
+
|
57
|
+
Logging.debug "Created SLO Logout Request: #{request}"
|
58
|
+
|
59
|
+
request = deflate(request) if settings.compress_request
|
60
|
+
base64_request = encode(request)
|
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
|
+
params['Signature'] = encode(signature)
|
74
|
+
end
|
75
|
+
|
76
|
+
params.each_pair do |key, value|
|
77
|
+
request_params[key] = value.to_s
|
78
|
+
end
|
79
|
+
|
80
|
+
request_params
|
81
|
+
end
|
82
|
+
|
83
|
+
# Creates the SAMLRequest String.
|
84
|
+
# @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
|
85
|
+
# @return [String] The SAMLRequest String.
|
86
|
+
#
|
87
|
+
def create_logout_request_xml_doc(settings)
|
88
|
+
time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
89
|
+
|
90
|
+
request_doc = XMLSecurity::Document.new
|
91
|
+
request_doc.uuid = uuid
|
92
|
+
|
93
|
+
root = request_doc.add_element "samlp:LogoutRequest", { "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol", "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion" }
|
94
|
+
root.attributes['ID'] = uuid
|
95
|
+
root.attributes['IssueInstant'] = time
|
96
|
+
root.attributes['Version'] = "2.0"
|
97
|
+
root.attributes['Destination'] = settings.idp_slo_target_url unless settings.idp_slo_target_url.nil?
|
98
|
+
|
99
|
+
if settings.issuer
|
100
|
+
issuer = root.add_element "saml:Issuer"
|
101
|
+
issuer.text = settings.issuer
|
102
|
+
end
|
103
|
+
|
104
|
+
nameid = root.add_element "saml:NameID"
|
105
|
+
if settings.name_identifier_value
|
106
|
+
nameid.attributes['NameQualifier'] = settings.sp_name_qualifier if settings.sp_name_qualifier
|
107
|
+
nameid.attributes['Format'] = settings.name_identifier_format if settings.name_identifier_format
|
108
|
+
nameid.text = settings.name_identifier_value
|
109
|
+
else
|
110
|
+
# If no NameID is present in the settings we generate one
|
111
|
+
nameid.text = "_" + UUID.new.generate
|
112
|
+
nameid.attributes['Format'] = 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
|
113
|
+
end
|
114
|
+
|
115
|
+
if settings.sessionindex
|
116
|
+
sessionindex = root.add_element "samlp:SessionIndex"
|
117
|
+
sessionindex.text = settings.sessionindex
|
118
|
+
end
|
119
|
+
|
120
|
+
# embed signature
|
121
|
+
if settings.security[:logout_requests_signed] && settings.private_key && settings.certificate && settings.security[:embed_sign]
|
122
|
+
private_key = settings.get_sp_key
|
123
|
+
cert = settings.get_sp_cert
|
124
|
+
request_doc.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
|
125
|
+
end
|
126
|
+
|
127
|
+
request_doc
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,241 @@
|
|
1
|
+
require "xml_security"
|
2
|
+
require "onelogin/ruby-saml/saml_message"
|
3
|
+
|
4
|
+
require "time"
|
5
|
+
|
6
|
+
# Only supports SAML 2.0
|
7
|
+
module OneLogin
|
8
|
+
module RubySaml
|
9
|
+
|
10
|
+
# SAML2 Logout Response (SLO IdP initiated, Parser)
|
11
|
+
#
|
12
|
+
class Logoutresponse < SamlMessage
|
13
|
+
|
14
|
+
# OneLogin::RubySaml::Settings Toolkit settings
|
15
|
+
attr_accessor :settings
|
16
|
+
|
17
|
+
# Array with the causes
|
18
|
+
attr_accessor :errors
|
19
|
+
|
20
|
+
attr_reader :document
|
21
|
+
attr_reader :response
|
22
|
+
attr_reader :options
|
23
|
+
|
24
|
+
attr_accessor :soft
|
25
|
+
|
26
|
+
# Constructs the Logout Response. A Logout Response Object that is an extension of the SamlMessage class.
|
27
|
+
# @param response [String] A UUEncoded logout response from the IdP.
|
28
|
+
# @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
|
29
|
+
# @param options [Hash] Extra parameters.
|
30
|
+
# :matches_request_id It will validate that the logout response matches the ID of the request.
|
31
|
+
# :get_params GET Parameters, including the SAMLResponse
|
32
|
+
# @raise [ArgumentError] if response is nil
|
33
|
+
#
|
34
|
+
def initialize(response, settings = nil, options = {})
|
35
|
+
@errors = []
|
36
|
+
raise ArgumentError.new("Logoutresponse cannot be nil") if response.nil?
|
37
|
+
@settings = settings
|
38
|
+
|
39
|
+
if settings.nil? || settings.soft.nil?
|
40
|
+
@soft = true
|
41
|
+
else
|
42
|
+
@soft = settings.soft
|
43
|
+
end
|
44
|
+
|
45
|
+
@options = options
|
46
|
+
@response = decode_raw_saml(response)
|
47
|
+
@document = XMLSecurity::SignedDocument.new(@response)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Append the cause to the errors array, and based on the value of soft, return false or raise
|
51
|
+
# an exception
|
52
|
+
def append_error(error_msg)
|
53
|
+
@errors << error_msg
|
54
|
+
return soft ? false : validation_error(error_msg)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Reset the errors array
|
58
|
+
def reset_errors!
|
59
|
+
@errors = []
|
60
|
+
end
|
61
|
+
|
62
|
+
# Checks if the Status has the "Success" code
|
63
|
+
# @return [Boolean] True if the StatusCode is Sucess
|
64
|
+
# @raise [ValidationError] if soft == false and validation fails
|
65
|
+
#
|
66
|
+
def success?
|
67
|
+
unless status_code == "urn:oasis:names:tc:SAML:2.0:status:Success"
|
68
|
+
return append_error("Bad status code. Expected <urn:oasis:names:tc:SAML:2.0:status:Success>, but was: <#@status_code>")
|
69
|
+
end
|
70
|
+
true
|
71
|
+
end
|
72
|
+
|
73
|
+
# @return [String|nil] Gets the InResponseTo attribute from the Logout Response if exists.
|
74
|
+
#
|
75
|
+
def in_response_to
|
76
|
+
@in_response_to ||= begin
|
77
|
+
node = REXML::XPath.first(
|
78
|
+
document,
|
79
|
+
"/p:LogoutResponse",
|
80
|
+
{ "p" => PROTOCOL, "a" => ASSERTION }
|
81
|
+
)
|
82
|
+
node.nil? ? nil : node.attributes['InResponseTo']
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# @return [String] Gets the Issuer from the Logout Response.
|
87
|
+
#
|
88
|
+
def issuer
|
89
|
+
@issuer ||= begin
|
90
|
+
node = REXML::XPath.first(
|
91
|
+
document,
|
92
|
+
"/p:LogoutResponse/a:Issuer",
|
93
|
+
{ "p" => PROTOCOL, "a" => ASSERTION }
|
94
|
+
)
|
95
|
+
node.nil? ? nil : node.text
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# @return [String] Gets the StatusCode from a Logout Response.
|
100
|
+
#
|
101
|
+
def status_code
|
102
|
+
@status_code ||= begin
|
103
|
+
node = REXML::XPath.first(document, "/p:LogoutResponse/p:Status/p:StatusCode", { "p" => PROTOCOL, "a" => ASSERTION })
|
104
|
+
node.nil? ? nil : node.attributes["Value"]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def status_message
|
109
|
+
@status_message ||= begin
|
110
|
+
node = REXML::XPath.first(
|
111
|
+
document,
|
112
|
+
"/p:LogoutResponse/p:Status/p:StatusMessage",
|
113
|
+
{ "p" => PROTOCOL, "a" => ASSERTION }
|
114
|
+
)
|
115
|
+
node.text if node
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Aux function to validate the Logout Response
|
120
|
+
# @return [Boolean] TRUE if the SAML Response is valid
|
121
|
+
# @raise [ValidationError] if soft == false and validation fails
|
122
|
+
#
|
123
|
+
def validate
|
124
|
+
reset_errors!
|
125
|
+
|
126
|
+
valid_state? &&
|
127
|
+
validate_success_status &&
|
128
|
+
validate_structure &&
|
129
|
+
valid_in_response_to? &&
|
130
|
+
valid_issuer? &&
|
131
|
+
validate_signature
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
# Validates the Status of the Logout Response
|
137
|
+
# If fails, the error is added to the errors array, including the StatusCode returned and the Status Message.
|
138
|
+
# @return [Boolean] True if the Logout Response contains a Success code, otherwise False if soft=True
|
139
|
+
# @raise [ValidationError] if soft == false and validation fails
|
140
|
+
#
|
141
|
+
def validate_success_status
|
142
|
+
return true if success?
|
143
|
+
|
144
|
+
error_msg = 'The status code of the Logout Response was not Success'
|
145
|
+
status_error_msg = OneLogin::RubySaml::Utils.status_error_msg(error_msg, status_code, status_message)
|
146
|
+
append_error(status_error_msg)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Validates the Logout Response against the specified schema.
|
150
|
+
# @return [Boolean] True if the XML is valid, otherwise False if soft=True
|
151
|
+
# @raise [ValidationError] if soft == false and validation fails
|
152
|
+
#
|
153
|
+
def validate_structure
|
154
|
+
unless valid_saml?(document, soft)
|
155
|
+
return append_error("Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd")
|
156
|
+
end
|
157
|
+
|
158
|
+
true
|
159
|
+
end
|
160
|
+
|
161
|
+
# Validates that the Logout Response provided in the initialization is not empty,
|
162
|
+
# also check that the setting and the IdP cert were also provided
|
163
|
+
# @return [Boolean] True if the required info is found, otherwise False if soft=True
|
164
|
+
# @raise [ValidationError] if soft == false and validation fails
|
165
|
+
#
|
166
|
+
def valid_state?
|
167
|
+
return append_error("Blank logout response") if response.empty?
|
168
|
+
|
169
|
+
return append_error("No settings on logout response") if settings.nil?
|
170
|
+
|
171
|
+
return append_error("No issuer in settings of the logout response") if settings.issuer.nil?
|
172
|
+
|
173
|
+
if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil?
|
174
|
+
return append_error("No fingerprint or certificate on settings of the logout response")
|
175
|
+
end
|
176
|
+
|
177
|
+
true
|
178
|
+
end
|
179
|
+
|
180
|
+
# Validates if a provided :matches_request_id matchs the inResponseTo value.
|
181
|
+
# @param soft [String|nil] request_id The ID of the Logout Request sent by this SP to the IdP (if was sent any)
|
182
|
+
# @return [Boolean] True if there is no request_id or it match, otherwise False if soft=True
|
183
|
+
# @raise [ValidationError] if soft == false and validation fails
|
184
|
+
#
|
185
|
+
def valid_in_response_to?
|
186
|
+
return true unless options.has_key? :matches_request_id
|
187
|
+
|
188
|
+
unless options[:matches_request_id] == in_response_to
|
189
|
+
return append_error("Response does not match the request ID, expected: <#{options[:matches_request_id]}>, but was: <#{in_response_to}>")
|
190
|
+
end
|
191
|
+
|
192
|
+
true
|
193
|
+
end
|
194
|
+
|
195
|
+
# Validates the Issuer of the Logout Response
|
196
|
+
# @return [Boolean] True if the Issuer matchs the IdP entityId, otherwise False if soft=True
|
197
|
+
# @raise [ValidationError] if soft == false and validation fails
|
198
|
+
#
|
199
|
+
def valid_issuer?
|
200
|
+
return true if settings.idp_entity_id.nil? || issuer.nil?
|
201
|
+
|
202
|
+
unless URI.parse(issuer) == URI.parse(settings.idp_entity_id)
|
203
|
+
return append_error("Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>")
|
204
|
+
end
|
205
|
+
true
|
206
|
+
end
|
207
|
+
|
208
|
+
# Validates the Signature if it exists and the GET parameters are provided
|
209
|
+
# @return [Boolean] True if not contains a Signature or if the Signature is valid, otherwise False if soft=True
|
210
|
+
# @raise [ValidationError] if soft == false and validation fails
|
211
|
+
#
|
212
|
+
def validate_signature
|
213
|
+
return true unless !options.nil?
|
214
|
+
return true unless options.has_key? :get_params
|
215
|
+
return true unless options[:get_params].has_key? 'Signature'
|
216
|
+
return true if settings.nil? || settings.get_idp_cert.nil?
|
217
|
+
|
218
|
+
query_string = OneLogin::RubySaml::Utils.build_query(
|
219
|
+
:type => 'SAMLResponse',
|
220
|
+
:data => options[:get_params]['SAMLResponse'],
|
221
|
+
:relay_state => options[:get_params]['RelayState'],
|
222
|
+
:sig_alg => options[:get_params]['SigAlg']
|
223
|
+
)
|
224
|
+
|
225
|
+
valid = OneLogin::RubySaml::Utils.verify_signature(
|
226
|
+
:cert => settings.get_idp_cert,
|
227
|
+
:sig_alg => options[:get_params]['SigAlg'],
|
228
|
+
:signature => options[:get_params]['Signature'],
|
229
|
+
:query_string => query_string
|
230
|
+
)
|
231
|
+
|
232
|
+
unless valid
|
233
|
+
error_msg = "Invalid Signature on Logout Response"
|
234
|
+
return append_error(error_msg)
|
235
|
+
end
|
236
|
+
true
|
237
|
+
end
|
238
|
+
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|