ruby-saml 0.9.4 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of ruby-saml might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/LICENSE +1 -1
- data/README.md +71 -15
- data/changelog.md +15 -6
- data/lib/onelogin/ruby-saml.rb +1 -0
- data/lib/onelogin/ruby-saml/attribute_service.rb +25 -2
- data/lib/onelogin/ruby-saml/attributes.rb +42 -23
- data/lib/onelogin/ruby-saml/authrequest.rb +33 -8
- data/lib/onelogin/ruby-saml/http_error.rb +7 -0
- data/lib/onelogin/ruby-saml/idp_metadata_parser.rb +65 -10
- data/lib/onelogin/ruby-saml/logging.rb +14 -10
- data/lib/onelogin/ruby-saml/logoutrequest.rb +39 -14
- data/lib/onelogin/ruby-saml/logoutresponse.rb +166 -39
- data/lib/onelogin/ruby-saml/metadata.rb +40 -23
- data/lib/onelogin/ruby-saml/response.rb +562 -88
- data/lib/onelogin/ruby-saml/saml_message.rb +80 -14
- data/lib/onelogin/ruby-saml/settings.rb +62 -23
- data/lib/onelogin/ruby-saml/slo_logoutrequest.rb +210 -20
- data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +44 -13
- data/lib/onelogin/ruby-saml/utils.rb +163 -40
- data/lib/onelogin/ruby-saml/version.rb +1 -1
- data/lib/schemas/saml-schema-metadata-2.0.xsd +0 -2
- data/lib/xml_security.rb +87 -29
- data/ruby-saml.gemspec +1 -0
- data/test/certificates/{r1_certificate2_base64 → certificate_without_head_foot} +0 -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/idp_metadata_parser_test.rb +41 -4
- data/test/logging_test.rb +62 -0
- data/test/logout_requests/invalid_slo_request.xml +6 -0
- data/test/{responses → logout_requests}/slo_request.xml +0 -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/{responses → logout_responses}/logoutresponse_fixtures.rb +6 -6
- data/test/logoutrequest_test.rb +79 -52
- data/test/logoutresponse_test.rb +206 -59
- data/test/metadata_test.rb +77 -7
- data/test/request_test.rb +80 -65
- data/test/response_test.rb +862 -189
- data/test/responses/attackxee.xml +13 -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/{response4.xml.base64 → response_assertion_wrapped.xml.base64} +0 -0
- data/test/responses/response_encrypted_nameid.xml.base64 +1 -0
- data/test/responses/response_unsigned_xml_base64 +1 -0
- data/test/responses/{response5.xml.base64 → response_with_saml2_namespace.xml.base64} +0 -0
- data/test/responses/{response3.xml.base64 → response_with_signed_assertion.xml.base64} +0 -0
- data/test/responses/{r1_response6.xml.base64 → response_with_signed_assertion_2.xml.base64} +0 -0
- data/test/responses/{response1.xml.base64 → response_with_undefined_recipient.xml.base64} +0 -0
- data/test/responses/{response2.xml.base64 → response_without_attributes.xml.base64} +0 -0
- data/test/responses/{wrapped_response_2.xml.base64 → response_wrapped.xml.base64} +0 -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/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 +138 -1
- data/test/slo_logoutrequest_test.rb +239 -28
- data/test/slo_logoutresponse_test.rb +93 -71
- data/test/test_helper.rb +138 -31
- data/test/utils_test.rb +129 -25
- data/test/xml_security_test.rb +140 -71
- metadata +142 -25
- data/test/responses/response_node_text_attack.xml.base64 +0 -1
@@ -7,22 +7,35 @@ require "net/https"
|
|
7
7
|
require "rexml/document"
|
8
8
|
require "rexml/xpath"
|
9
9
|
|
10
|
+
# Only supports SAML 2.0
|
10
11
|
module OneLogin
|
11
12
|
module RubySaml
|
12
13
|
include REXML
|
13
14
|
|
15
|
+
# Auxiliary class to retrieve and parse the Identity Provider Metadata
|
16
|
+
#
|
14
17
|
class IdpMetadataParser
|
15
18
|
|
16
19
|
METADATA = "urn:oasis:names:tc:SAML:2.0:metadata"
|
17
20
|
DSIG = "http://www.w3.org/2000/09/xmldsig#"
|
18
21
|
|
19
22
|
attr_reader :document
|
20
|
-
|
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)
|
21
31
|
def parse_remote(url, validate_cert = true)
|
22
32
|
idp_metadata = get_idp_metadata(url, validate_cert)
|
23
33
|
parse(idp_metadata)
|
24
34
|
end
|
25
35
|
|
36
|
+
# Parse the Identity Provider metadata and update the settings with the IdP values
|
37
|
+
# @param idp_metadata [String]
|
38
|
+
#
|
26
39
|
def parse(idp_metadata)
|
27
40
|
@document = REXML::Document.new(idp_metadata)
|
28
41
|
|
@@ -37,8 +50,11 @@ module OneLogin
|
|
37
50
|
|
38
51
|
private
|
39
52
|
|
40
|
-
# Retrieve the remote IdP metadata from the URL or a cached copy
|
41
|
-
#
|
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
|
42
58
|
def get_idp_metadata(url, validate_cert)
|
43
59
|
uri = URI.parse(url)
|
44
60
|
if uri.scheme == "http"
|
@@ -62,37 +78,76 @@ module OneLogin
|
|
62
78
|
get = Net::HTTP::Get.new(uri.request_uri)
|
63
79
|
response = http.request(get)
|
64
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")
|
65
87
|
end
|
88
|
+
|
66
89
|
meta_text
|
67
90
|
end
|
68
91
|
|
92
|
+
# @return [String|nil] IdP Entity ID value if exists
|
93
|
+
#
|
69
94
|
def idp_entity_id
|
70
|
-
node = REXML::XPath.first(
|
95
|
+
node = REXML::XPath.first(
|
96
|
+
document,
|
97
|
+
"/md:EntityDescriptor/@entityID",
|
98
|
+
{ "md" => METADATA }
|
99
|
+
)
|
71
100
|
node.value if node
|
72
101
|
end
|
73
102
|
|
103
|
+
# @return [String|nil] IdP Name ID Format value if exists
|
104
|
+
#
|
74
105
|
def idp_name_id_format
|
75
|
-
node = REXML::XPath.first(
|
76
|
-
|
106
|
+
node = REXML::XPath.first(
|
107
|
+
document,
|
108
|
+
"/md:EntityDescriptor/md:IDPSSODescriptor/md:NameIDFormat",
|
109
|
+
{ "md" => METADATA }
|
110
|
+
)
|
111
|
+
node.text if node
|
77
112
|
end
|
78
113
|
|
114
|
+
# @return [String|nil] SingleSignOnService endpoint if exists
|
115
|
+
#
|
79
116
|
def single_signon_service_url
|
80
|
-
node = REXML::XPath.first(
|
117
|
+
node = REXML::XPath.first(
|
118
|
+
document,
|
119
|
+
"/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleSignOnService/@Location",
|
120
|
+
{ "md" => METADATA }
|
121
|
+
)
|
81
122
|
node.value if node
|
82
123
|
end
|
83
124
|
|
125
|
+
# @return [String|nil] SingleLogoutService endpoint if exists
|
126
|
+
#
|
84
127
|
def single_logout_service_url
|
85
|
-
node = REXML::XPath.first(
|
128
|
+
node = REXML::XPath.first(
|
129
|
+
document,
|
130
|
+
"/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleLogoutService/@Location",
|
131
|
+
{ "md" => METADATA }
|
132
|
+
)
|
86
133
|
node.value if node
|
87
134
|
end
|
88
135
|
|
136
|
+
# @return [String|nil] X509Certificate if exists
|
137
|
+
#
|
89
138
|
def certificate
|
90
139
|
@certificate ||= begin
|
91
|
-
node = REXML::XPath.first(
|
92
|
-
|
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
|
93
146
|
end
|
94
147
|
end
|
95
148
|
|
149
|
+
# @return [String|nil] the SHA-1 fingerpint of the X509Certificate if it exists
|
150
|
+
#
|
96
151
|
def fingerprint
|
97
152
|
@fingerprint ||= begin
|
98
153
|
if certificate
|
@@ -1,25 +1,29 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
1
3
|
# Simplistic log class when we're running in Rails
|
2
4
|
module OneLogin
|
3
5
|
module RubySaml
|
4
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
|
+
|
5
17
|
def self.debug(message)
|
6
18
|
return if !!ENV["ruby-saml/testing"]
|
7
19
|
|
8
|
-
|
9
|
-
Rails.logger.debug message
|
10
|
-
else
|
11
|
-
puts message
|
12
|
-
end
|
20
|
+
logger.debug message
|
13
21
|
end
|
14
22
|
|
15
23
|
def self.info(message)
|
16
24
|
return if !!ENV["ruby-saml/testing"]
|
17
25
|
|
18
|
-
|
19
|
-
Rails.logger.info message
|
20
|
-
else
|
21
|
-
puts message
|
22
|
-
end
|
26
|
+
logger.info message
|
23
27
|
end
|
24
28
|
end
|
25
29
|
end
|
@@ -3,16 +3,29 @@ require "uuid"
|
|
3
3
|
require "onelogin/ruby-saml/logging"
|
4
4
|
require "onelogin/ruby-saml/saml_message"
|
5
5
|
|
6
|
+
# Only supports SAML 2.0
|
6
7
|
module OneLogin
|
7
8
|
module RubySaml
|
9
|
+
|
10
|
+
# SAML2 Logout Request (SLO SP initiated, Builder)
|
11
|
+
#
|
8
12
|
class Logoutrequest < SamlMessage
|
9
13
|
|
10
|
-
|
14
|
+
# Logout Request ID
|
15
|
+
attr_reader :uuid
|
11
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
|
+
#
|
12
20
|
def initialize
|
13
21
|
@uuid = "_" + UUID.new.generate
|
14
22
|
end
|
15
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
|
+
#
|
16
29
|
def create(settings, params={})
|
17
30
|
params = create_params(settings, params)
|
18
31
|
params_prefix = (settings.idp_slo_target_url =~ /\?/) ? '&' : '?'
|
@@ -24,6 +37,11 @@ module OneLogin
|
|
24
37
|
@logout_url = settings.idp_slo_target_url + request_params
|
25
38
|
end
|
26
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
|
+
#
|
27
45
|
def create_params(settings, params={})
|
28
46
|
# The method expects :RelayState but sometimes we get 'RelayState' instead.
|
29
47
|
# Based on the HashWithIndifferentAccess value in Rails we could experience
|
@@ -44,11 +62,14 @@ module OneLogin
|
|
44
62
|
|
45
63
|
if settings.security[:logout_requests_signed] && !settings.security[:embed_sign] && settings.private_key
|
46
64
|
params['SigAlg'] = settings.security[:signature_method]
|
47
|
-
url_string
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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)
|
52
73
|
params['Signature'] = encode(signature)
|
53
74
|
end
|
54
75
|
|
@@ -59,6 +80,10 @@ module OneLogin
|
|
59
80
|
request_params
|
60
81
|
end
|
61
82
|
|
83
|
+
# Creates the SAMLRequest String.
|
84
|
+
# @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
|
85
|
+
# @return [String] The SAMLRequest String.
|
86
|
+
#
|
62
87
|
def create_logout_request_xml_doc(settings)
|
63
88
|
time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
64
89
|
|
@@ -76,15 +101,15 @@ module OneLogin
|
|
76
101
|
issuer.text = settings.issuer
|
77
102
|
end
|
78
103
|
|
79
|
-
|
104
|
+
nameid = root.add_element "saml:NameID"
|
80
105
|
if settings.name_identifier_value
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
84
109
|
else
|
85
110
|
# If no NameID is present in the settings we generate one
|
86
|
-
|
87
|
-
|
111
|
+
nameid.text = "_" + UUID.new.generate
|
112
|
+
nameid.attributes['Format'] = 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
|
88
113
|
end
|
89
114
|
|
90
115
|
if settings.sessionindex
|
@@ -94,8 +119,8 @@ module OneLogin
|
|
94
119
|
|
95
120
|
# embed signature
|
96
121
|
if settings.security[:logout_requests_signed] && settings.private_key && settings.certificate && settings.security[:embed_sign]
|
97
|
-
private_key = settings.get_sp_key
|
98
|
-
cert = settings.get_sp_cert
|
122
|
+
private_key = settings.get_sp_key
|
123
|
+
cert = settings.get_sp_cert
|
99
124
|
request_doc.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
|
100
125
|
end
|
101
126
|
|
@@ -3,65 +3,101 @@ require "onelogin/ruby-saml/saml_message"
|
|
3
3
|
|
4
4
|
require "time"
|
5
5
|
|
6
|
+
# Only supports SAML 2.0
|
6
7
|
module OneLogin
|
7
8
|
module RubySaml
|
9
|
+
|
10
|
+
# SAML2 Logout Response (SLO IdP initiated, Parser)
|
11
|
+
#
|
8
12
|
class Logoutresponse < SamlMessage
|
9
|
-
|
13
|
+
|
14
|
+
# OneLogin::RubySaml::Settings Toolkit settings
|
10
15
|
attr_accessor :settings
|
11
16
|
|
17
|
+
# Array with the causes
|
18
|
+
attr_accessor :errors
|
19
|
+
|
12
20
|
attr_reader :document
|
13
21
|
attr_reader :response
|
14
22
|
attr_reader :options
|
15
23
|
|
16
|
-
|
17
|
-
|
18
|
-
# the
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
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
|
23
33
|
#
|
24
34
|
def initialize(response, settings = nil, options = {})
|
35
|
+
@errors = []
|
25
36
|
raise ArgumentError.new("Logoutresponse cannot be nil") if response.nil?
|
26
|
-
|
37
|
+
@settings = settings
|
38
|
+
|
39
|
+
if settings.nil? || settings.soft.nil?
|
40
|
+
@soft = true
|
41
|
+
else
|
42
|
+
@soft = settings.soft
|
43
|
+
end
|
27
44
|
|
28
45
|
@options = options
|
29
46
|
@response = decode_raw_saml(response)
|
30
47
|
@document = XMLSecurity::SignedDocument.new(@response)
|
31
48
|
end
|
32
49
|
|
33
|
-
|
34
|
-
|
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)
|
35
55
|
end
|
36
56
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
valid_in_response_to?(soft) && valid_issuer?(soft) && success?(soft)
|
57
|
+
# Reset the errors array
|
58
|
+
def reset_errors!
|
59
|
+
@errors = []
|
41
60
|
end
|
42
61
|
|
43
|
-
|
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?
|
44
67
|
unless status_code == "urn:oasis:names:tc:SAML:2.0:status:Success"
|
45
|
-
return
|
68
|
+
return append_error("Bad status code. Expected <urn:oasis:names:tc:SAML:2.0:status:Success>, but was: <#@status_code>")
|
46
69
|
end
|
47
70
|
true
|
48
71
|
end
|
49
72
|
|
73
|
+
# @return [String|nil] Gets the InResponseTo attribute from the Logout Response if exists.
|
74
|
+
#
|
50
75
|
def in_response_to
|
51
76
|
@in_response_to ||= begin
|
52
|
-
node = REXML::XPath.first(
|
77
|
+
node = REXML::XPath.first(
|
78
|
+
document,
|
79
|
+
"/p:LogoutResponse",
|
80
|
+
{ "p" => PROTOCOL, "a" => ASSERTION }
|
81
|
+
)
|
53
82
|
node.nil? ? nil : node.attributes['InResponseTo']
|
54
83
|
end
|
55
84
|
end
|
56
85
|
|
86
|
+
# @return [String] Gets the Issuer from the Logout Response.
|
87
|
+
#
|
57
88
|
def issuer
|
58
89
|
@issuer ||= begin
|
59
|
-
node = REXML::XPath.first(
|
60
|
-
|
61
|
-
|
90
|
+
node = REXML::XPath.first(
|
91
|
+
document,
|
92
|
+
"/p:LogoutResponse/a:Issuer",
|
93
|
+
{ "p" => PROTOCOL, "a" => ASSERTION }
|
94
|
+
)
|
95
|
+
node.nil? ? nil : node.text
|
62
96
|
end
|
63
97
|
end
|
64
98
|
|
99
|
+
# @return [String] Gets the StatusCode from a Logout Response.
|
100
|
+
#
|
65
101
|
def status_code
|
66
102
|
@status_code ||= begin
|
67
103
|
node = REXML::XPath.first(document, "/p:LogoutResponse/p:Status/p:StatusCode", { "p" => PROTOCOL, "a" => ASSERTION })
|
@@ -69,46 +105,137 @@ module OneLogin
|
|
69
105
|
end
|
70
106
|
end
|
71
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
|
+
|
72
134
|
private
|
73
135
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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?
|
78
143
|
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
82
148
|
|
83
|
-
|
84
|
-
|
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")
|
85
156
|
end
|
86
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
|
+
|
87
173
|
if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil?
|
88
|
-
return
|
174
|
+
return append_error("No fingerprint or certificate on settings of the logout response")
|
89
175
|
end
|
90
176
|
|
91
177
|
true
|
92
178
|
end
|
93
179
|
|
94
|
-
|
95
|
-
|
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
|
96
187
|
|
97
|
-
unless
|
98
|
-
return
|
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}>")
|
99
190
|
end
|
100
191
|
|
101
192
|
true
|
102
193
|
end
|
103
194
|
|
104
|
-
|
105
|
-
|
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?
|
106
201
|
|
107
|
-
unless URI.parse(
|
108
|
-
return
|
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}>")
|
109
204
|
end
|
110
205
|
true
|
111
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
|
+
|
112
239
|
end
|
113
240
|
end
|
114
241
|
end
|