kl-ruby-saml 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/.gitignore +14 -0
- data/.travis.yml +17 -0
- data/Gemfile +9 -0
- data/LICENSE +19 -0
- data/README.md +575 -0
- data/Rakefile +41 -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 +156 -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 +722 -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 +358 -0
- data/ruby-saml.gemspec +57 -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 +1094 -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_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/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 +252 -0
- data/test/utils_test.rb +145 -0
- data/test/xml_security_test.rb +329 -0
- metadata +415 -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
|