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
@@ -6,8 +6,12 @@ require 'rexml/document'
|
|
6
6
|
require 'rexml/xpath'
|
7
7
|
require 'thread'
|
8
8
|
|
9
|
+
# Only supports SAML 2.0
|
9
10
|
module OneLogin
|
10
11
|
module RubySaml
|
12
|
+
|
13
|
+
# SAML2 Message
|
14
|
+
#
|
11
15
|
class SamlMessage
|
12
16
|
include REXML
|
13
17
|
|
@@ -16,6 +20,8 @@ module OneLogin
|
|
16
20
|
|
17
21
|
BASE64_FORMAT = %r(\A[A-Za-z0-9+/]{4}*[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=?\Z)
|
18
22
|
|
23
|
+
# @return [Nokogiri::XML::Schema] Gets the schema object of the SAML 2.0 Protocol schema
|
24
|
+
#
|
19
25
|
def self.schema
|
20
26
|
@schema ||= Mutex.new.synchronize do
|
21
27
|
Dir.chdir(File.expand_path("../../../schemas", __FILE__)) do
|
@@ -24,29 +30,68 @@ module OneLogin
|
|
24
30
|
end
|
25
31
|
end
|
26
32
|
|
33
|
+
# @return [String|nil] Gets the Version attribute from the SAML Message if exists.
|
34
|
+
#
|
35
|
+
def version(document)
|
36
|
+
@version ||= begin
|
37
|
+
node = REXML::XPath.first(
|
38
|
+
document,
|
39
|
+
"/p:AuthnRequest | /p:Response | /p:LogoutResponse | /p:LogoutRequest",
|
40
|
+
{ "p" => PROTOCOL }
|
41
|
+
)
|
42
|
+
node.nil? ? nil : node.attributes['Version']
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return [String|nil] Gets the ID attribute from the SAML Message if exists.
|
47
|
+
#
|
48
|
+
def id(document)
|
49
|
+
@id ||= begin
|
50
|
+
node = REXML::XPath.first(
|
51
|
+
document,
|
52
|
+
"/p:AuthnRequest | /p:Response | /p:LogoutResponse | /p:LogoutRequest",
|
53
|
+
{ "p" => PROTOCOL }
|
54
|
+
)
|
55
|
+
node.nil? ? nil : node.attributes['ID']
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Validates the SAML Message against the specified schema.
|
60
|
+
# @param document [REXML::Document] The message that will be validated
|
61
|
+
# @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the message is invalid or not)
|
62
|
+
# @return [Boolean] True if the XML is valid, otherwise False, if soft=True
|
63
|
+
# @raise [ValidationError] if soft == false and validation fails
|
64
|
+
#
|
27
65
|
def valid_saml?(document, soft = true)
|
28
|
-
|
66
|
+
begin
|
67
|
+
xml = Nokogiri::XML(document.to_s) do |config|
|
68
|
+
config.options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
|
69
|
+
end
|
70
|
+
rescue Exception => error
|
71
|
+
return false if soft
|
72
|
+
validation_error("XML load failed: #{error.message}")
|
73
|
+
end
|
29
74
|
|
30
75
|
SamlMessage.schema.validate(xml).map do |error|
|
31
|
-
|
76
|
+
return false if soft
|
32
77
|
validation_error("#{error.message}\n\n#{xml.to_s}")
|
33
78
|
end
|
34
79
|
end
|
35
80
|
|
81
|
+
# Raise a ValidationError with the provided message
|
82
|
+
# @param message [String] Message of the exception
|
83
|
+
# @raise [ValidationError]
|
84
|
+
#
|
36
85
|
def validation_error(message)
|
37
86
|
raise ValidationError.new(message)
|
38
87
|
end
|
39
88
|
|
40
89
|
private
|
41
90
|
|
42
|
-
|
43
|
-
#
|
44
|
-
#
|
91
|
+
# Base64 decode and try also to inflate a SAML Message
|
92
|
+
# @param saml [String] The deflated and encoded SAML Message
|
93
|
+
# @return [String] The plain SAML Message
|
45
94
|
#
|
46
|
-
# Since SAML decided to use the RFC1951 and therefor has no zlib markers,
|
47
|
-
# the only reliable method of deciding whether we have a zlib stream or not
|
48
|
-
# is to try and inflate it and fall back to the base64 decoded string if
|
49
|
-
# the stream contains errors.
|
50
95
|
def decode_raw_saml(saml)
|
51
96
|
return saml unless base64_encoded?(saml)
|
52
97
|
|
@@ -58,32 +103,53 @@ module OneLogin
|
|
58
103
|
end
|
59
104
|
end
|
60
105
|
|
106
|
+
# Deflate, base64 encode and url-encode a SAML Message (To be used in the HTTP-redirect binding)
|
107
|
+
# @param saml [String] The plain SAML Message
|
108
|
+
# @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
|
109
|
+
# @return [String] The deflated and encoded SAML Message (encoded if the compression is requested)
|
110
|
+
#
|
61
111
|
def encode_raw_saml(saml, settings)
|
62
112
|
saml = deflate(saml) if settings.compress_request
|
63
113
|
|
64
114
|
CGI.escape(Base64.encode64(saml))
|
65
115
|
end
|
66
116
|
|
67
|
-
|
68
|
-
|
117
|
+
# Base 64 decode method
|
118
|
+
# @param string [String] The string message
|
119
|
+
# @return [String] The decoded string
|
120
|
+
#
|
121
|
+
def decode(string)
|
122
|
+
Base64.decode64(string)
|
69
123
|
end
|
70
124
|
|
71
|
-
|
72
|
-
|
125
|
+
# Base 64 encode method
|
126
|
+
# @param string [String] The string
|
127
|
+
# @return [String] The encoded string
|
128
|
+
#
|
129
|
+
def encode(string)
|
130
|
+
Base64.encode64(string).gsub(/\n/, "")
|
73
131
|
end
|
74
132
|
|
75
133
|
# Check if a string is base64 encoded
|
76
|
-
#
|
77
134
|
# @param string [String] string to check the encoding of
|
78
135
|
# @return [true, false] whether or not the string is base64 encoded
|
136
|
+
#
|
79
137
|
def base64_encoded?(string)
|
80
138
|
!!string.gsub(/[\r\n]|\\r|\\n/, "").match(BASE64_FORMAT)
|
81
139
|
end
|
82
140
|
|
141
|
+
# Inflate method
|
142
|
+
# @param deflated [String] The string
|
143
|
+
# @return [String] The inflated string
|
144
|
+
#
|
83
145
|
def inflate(deflated)
|
84
146
|
Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(deflated)
|
85
147
|
end
|
86
148
|
|
149
|
+
# Deflate method
|
150
|
+
# @param inflated [String] The string
|
151
|
+
# @return [String] The deflated string
|
152
|
+
#
|
87
153
|
def deflate(inflated)
|
88
154
|
Zlib::Deflate.deflate(inflated, 9)[2..-5]
|
89
155
|
end
|
@@ -2,16 +2,20 @@ require "xml_security"
|
|
2
2
|
require "onelogin/ruby-saml/attribute_service"
|
3
3
|
require "onelogin/ruby-saml/utils"
|
4
4
|
|
5
|
+
# Only supports SAML 2.0
|
5
6
|
module OneLogin
|
6
7
|
module RubySaml
|
8
|
+
|
9
|
+
# SAML2 Toolkit Settings
|
10
|
+
#
|
7
11
|
class Settings
|
8
12
|
def initialize(overrides = {})
|
9
13
|
config = DEFAULTS.merge(overrides)
|
10
14
|
config.each do |k,v|
|
11
15
|
acc = "#{k.to_s}=".to_sym
|
12
|
-
if
|
16
|
+
if respond_to? acc
|
13
17
|
value = v.is_a?(Hash) ? v.dup : v
|
14
|
-
|
18
|
+
send(acc, value)
|
15
19
|
end
|
16
20
|
end
|
17
21
|
@attribute_consuming_service = AttributeService.new
|
@@ -39,18 +43,22 @@ module OneLogin
|
|
39
43
|
attr_accessor :protocol_binding
|
40
44
|
attr_accessor :attributes_index
|
41
45
|
attr_accessor :force_authn
|
42
|
-
attr_accessor :security
|
43
46
|
attr_accessor :certificate
|
44
47
|
attr_accessor :private_key
|
45
48
|
attr_accessor :authn_context
|
46
49
|
attr_accessor :authn_context_comparison
|
47
50
|
attr_accessor :authn_context_decl_ref
|
48
51
|
attr_reader :attribute_consuming_service
|
52
|
+
# Work-flow
|
53
|
+
attr_accessor :security
|
54
|
+
attr_accessor :soft
|
49
55
|
# Compability
|
50
56
|
attr_accessor :assertion_consumer_logout_service_url
|
51
57
|
attr_accessor :assertion_consumer_logout_service_binding
|
52
58
|
|
53
|
-
|
59
|
+
# @return [String] Single Logout Service URL.
|
60
|
+
#
|
61
|
+
def single_logout_service_url
|
54
62
|
val = nil
|
55
63
|
if @single_logout_service_url.nil?
|
56
64
|
if @assertion_consumer_logout_service_url
|
@@ -62,12 +70,16 @@ module OneLogin
|
|
62
70
|
val
|
63
71
|
end
|
64
72
|
|
65
|
-
#
|
66
|
-
|
67
|
-
|
73
|
+
# Setter for the Single Logout Service URL.
|
74
|
+
# @param url [String].
|
75
|
+
#
|
76
|
+
def single_logout_service_url=(url)
|
77
|
+
@single_logout_service_url = url
|
68
78
|
end
|
69
79
|
|
70
|
-
|
80
|
+
# @return [String] Single Logout Service Binding.
|
81
|
+
#
|
82
|
+
def single_logout_service_binding
|
71
83
|
val = nil
|
72
84
|
if @single_logout_service_binding.nil?
|
73
85
|
if @assertion_consumer_logout_service_binding
|
@@ -79,27 +91,53 @@ module OneLogin
|
|
79
91
|
val
|
80
92
|
end
|
81
93
|
|
82
|
-
#
|
83
|
-
|
84
|
-
|
94
|
+
# Setter for Single Logout Service Binding.
|
95
|
+
#
|
96
|
+
# (Currently we only support "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect")
|
97
|
+
# @param url [String]
|
98
|
+
#
|
99
|
+
def single_logout_service_binding=(url)
|
100
|
+
@single_logout_service_binding = url
|
85
101
|
end
|
86
102
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
103
|
+
# Calculates the fingerprint of the IdP x509 certificate.
|
104
|
+
# @return [String] The fingerprint
|
105
|
+
#
|
106
|
+
def get_fingerprint
|
107
|
+
idp_cert_fingerprint || begin
|
108
|
+
idp_cert = get_idp_cert
|
109
|
+
if idp_cert
|
110
|
+
fingerprint_alg = XMLSecurity::BaseDocument.new.algorithm(idp_cert_fingerprint_algorithm).new
|
111
|
+
fingerprint_alg.hexdigest(idp_cert.to_der).upcase.scan(/../).join(":")
|
112
|
+
end
|
92
113
|
end
|
93
|
-
cert
|
94
114
|
end
|
95
115
|
|
116
|
+
# @return [OpenSSL::X509::Certificate|nil] Build the IdP certificate from the settings (previously format it)
|
117
|
+
#
|
118
|
+
def get_idp_cert
|
119
|
+
return nil if idp_cert.nil? || idp_cert.empty?
|
120
|
+
|
121
|
+
formated_cert = OneLogin::RubySaml::Utils.format_cert(idp_cert)
|
122
|
+
OpenSSL::X509::Certificate.new(formated_cert)
|
123
|
+
end
|
124
|
+
|
125
|
+
# @return [OpenSSL::X509::Certificate|nil] Build the SP certificate from the settings (previously format it)
|
126
|
+
#
|
127
|
+
def get_sp_cert
|
128
|
+
return nil if certificate.nil? || certificate.empty?
|
129
|
+
|
130
|
+
formated_cert = OneLogin::RubySaml::Utils.format_cert(certificate)
|
131
|
+
OpenSSL::X509::Certificate.new(formated_cert)
|
132
|
+
end
|
133
|
+
|
134
|
+
# @return [OpenSSL::PKey::RSA] Build the SP private from the settings (previously format it)
|
135
|
+
#
|
96
136
|
def get_sp_key
|
97
|
-
private_key
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
end
|
102
|
-
private_key
|
137
|
+
return nil if private_key.nil? || private_key.empty?
|
138
|
+
|
139
|
+
formated_private_key = OneLogin::RubySaml::Utils.format_private_key(private_key)
|
140
|
+
OpenSSL::PKey::RSA.new(formated_private_key)
|
103
141
|
end
|
104
142
|
|
105
143
|
private
|
@@ -110,6 +148,7 @@ module OneLogin
|
|
110
148
|
:idp_cert_fingerprint_algorithm => XMLSecurity::Document::SHA1,
|
111
149
|
:compress_request => true,
|
112
150
|
:compress_response => true,
|
151
|
+
:soft => true,
|
113
152
|
:security => {
|
114
153
|
:authn_requests_signed => false,
|
115
154
|
:logout_requests_signed => false,
|
@@ -7,59 +7,249 @@ require "onelogin/ruby-saml/saml_message"
|
|
7
7
|
# Only supports SAML 2.0
|
8
8
|
module OneLogin
|
9
9
|
module RubySaml
|
10
|
+
|
11
|
+
# SAML2 Logout Request (SLO IdP initiated, Parser)
|
12
|
+
#
|
10
13
|
class SloLogoutrequest < SamlMessage
|
11
|
-
|
12
|
-
|
14
|
+
|
15
|
+
# OneLogin::RubySaml::Settings Toolkit settings
|
16
|
+
attr_accessor :settings
|
17
|
+
|
18
|
+
# Array with the causes [Array of strings]
|
19
|
+
attr_accessor :errors
|
20
|
+
|
13
21
|
attr_reader :document
|
22
|
+
attr_reader :request
|
23
|
+
attr_reader :options
|
14
24
|
|
25
|
+
attr_accessor :soft
|
26
|
+
|
27
|
+
# Constructs the Logout Request. A Logout Request Object that is an extension of the SamlMessage class.
|
28
|
+
# @param request [String] A UUEncoded Logout Request from the IdP.
|
29
|
+
# @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object
|
30
|
+
# Or :allowed_clock_drift for the logout request validation process to allow a clock drift when checking dates with
|
31
|
+
#
|
32
|
+
# @raise [ArgumentError] If Request is nil
|
33
|
+
#
|
15
34
|
def initialize(request, options = {})
|
35
|
+
@errors = []
|
16
36
|
raise ArgumentError.new("Request cannot be nil") if request.nil?
|
17
37
|
@options = options
|
38
|
+
|
39
|
+
@soft = true
|
40
|
+
if !options.empty? && !options[:settings].nil?
|
41
|
+
@settings = options[:settings]
|
42
|
+
if !options[:settings].soft.nil?
|
43
|
+
@soft = options[:settings].soft
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
18
47
|
@request = decode_raw_saml(request)
|
19
48
|
@document = REXML::Document.new(@request)
|
20
49
|
end
|
21
50
|
|
22
|
-
|
23
|
-
|
51
|
+
# Append the cause to the errors array, and based on the value of soft, return false or raise
|
52
|
+
# an exception
|
53
|
+
def append_error(error_msg)
|
54
|
+
@errors << error_msg
|
55
|
+
return soft ? false : validation_error(error_msg)
|
24
56
|
end
|
25
57
|
|
26
|
-
|
27
|
-
|
58
|
+
# Reset the errors array
|
59
|
+
def reset_errors!
|
60
|
+
@errors = []
|
28
61
|
end
|
29
62
|
|
30
|
-
#
|
63
|
+
# Validates the Logout Request with the default values (soft = true)
|
64
|
+
# @return [Boolean] TRUE if the Logout Request is valid
|
65
|
+
#
|
66
|
+
def is_valid?
|
67
|
+
validate
|
68
|
+
end
|
69
|
+
|
70
|
+
# @return [String] Gets the NameID of the Logout Request.
|
71
|
+
#
|
31
72
|
def name_id
|
32
73
|
@name_id ||= begin
|
33
74
|
node = REXML::XPath.first(document, "/p:LogoutRequest/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION })
|
34
|
-
|
75
|
+
node.nil? ? nil : node.text
|
35
76
|
end
|
36
77
|
end
|
37
78
|
|
79
|
+
alias_method :nameid, :name_id
|
80
|
+
|
81
|
+
# @return [String|nil] Gets the ID attribute from the Logout Request. if exists.
|
82
|
+
#
|
38
83
|
def id
|
39
|
-
|
40
|
-
element = REXML::XPath.first(document, "/p:LogoutRequest", {
|
41
|
-
"p" => PROTOCOL} )
|
42
|
-
return nil if element.nil?
|
43
|
-
return element.attributes["ID"]
|
84
|
+
super(document)
|
44
85
|
end
|
45
86
|
|
87
|
+
# @return [String] Gets the Issuer from the Logout Request.
|
88
|
+
#
|
46
89
|
def issuer
|
47
90
|
@issuer ||= begin
|
48
|
-
node = REXML::XPath.first(
|
49
|
-
|
91
|
+
node = REXML::XPath.first(
|
92
|
+
document,
|
93
|
+
"/p:LogoutRequest/a:Issuer",
|
94
|
+
{ "p" => PROTOCOL, "a" => ASSERTION }
|
95
|
+
)
|
96
|
+
node.nil? ? nil : node.text
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# @return [Time|nil] Gets the NotOnOrAfter Attribute value if exists.
|
101
|
+
#
|
102
|
+
def not_on_or_after
|
103
|
+
@not_on_or_after ||= begin
|
104
|
+
node = REXML::XPath.first(
|
105
|
+
document,
|
106
|
+
"/p:LogoutRequest",
|
107
|
+
{ "p" => PROTOCOL }
|
108
|
+
)
|
109
|
+
if node && node.attributes["NotOnOrAfter"]
|
110
|
+
Time.parse(node.attributes["NotOnOrAfter"])
|
111
|
+
end
|
50
112
|
end
|
51
113
|
end
|
52
114
|
|
115
|
+
# @return [Array] Gets the SessionIndex if exists (Supported multiple values). Empty Array if none found
|
116
|
+
#
|
117
|
+
def session_indexes
|
118
|
+
s_indexes = []
|
119
|
+
nodes = REXML::XPath.match(
|
120
|
+
document,
|
121
|
+
"/p:LogoutRequest/p:SessionIndex",
|
122
|
+
{ "p" => PROTOCOL }
|
123
|
+
)
|
124
|
+
|
125
|
+
nodes.each do |node|
|
126
|
+
s_indexes << node.text
|
127
|
+
end
|
128
|
+
|
129
|
+
s_indexes
|
130
|
+
end
|
131
|
+
|
53
132
|
private
|
54
133
|
|
55
|
-
|
56
|
-
|
134
|
+
# Hard aux function to validate the Logout Request
|
135
|
+
# @return [Boolean] TRUE if the Logout Request is valid
|
136
|
+
# @raise [ValidationError] if soft == false and validation fails
|
137
|
+
#
|
138
|
+
def validate
|
139
|
+
reset_errors!
|
140
|
+
|
141
|
+
validate_request_state &&
|
142
|
+
validate_id &&
|
143
|
+
validate_version &&
|
144
|
+
validate_structure &&
|
145
|
+
validate_not_on_or_after &&
|
146
|
+
validate_issuer &&
|
147
|
+
validate_signature
|
148
|
+
end
|
149
|
+
|
150
|
+
# Validates that the Logout Request contains an ID
|
151
|
+
# If fails, the error is added to the errors array.
|
152
|
+
# @return [Boolean] True if the Logout Request contains an ID, otherwise returns False
|
153
|
+
#
|
154
|
+
def validate_id
|
155
|
+
unless id
|
156
|
+
return append_error("Missing ID attribute on Logout Request")
|
157
|
+
end
|
158
|
+
|
159
|
+
true
|
160
|
+
end
|
161
|
+
|
162
|
+
# Validates the SAML version (2.0)
|
163
|
+
# If fails, the error is added to the errors array.
|
164
|
+
# @return [Boolean] True if the Logout Request is 2.0, otherwise returns False
|
165
|
+
#
|
166
|
+
def validate_version
|
167
|
+
unless version(document) == "2.0"
|
168
|
+
return append_error("Unsupported SAML version")
|
169
|
+
end
|
170
|
+
|
171
|
+
true
|
172
|
+
end
|
173
|
+
|
174
|
+
# Validates the time. (If the logout request was initialized with the :allowed_clock_drift option, the timing validations are relaxed by the allowed_clock_drift value)
|
175
|
+
# If fails, the error is added to the errors array
|
176
|
+
# @return [Boolean] True if satisfies the conditions, otherwise False if soft=True
|
177
|
+
# @raise [ValidationError] if soft == false and validation fails
|
178
|
+
#
|
179
|
+
def validate_not_on_or_after
|
180
|
+
now = Time.now.utc
|
181
|
+
if not_on_or_after && now >= (not_on_or_after + (options[:allowed_clock_drift] || 0))
|
182
|
+
return append_error("Current time is on or after NotOnOrAfter (#{now} >= #{not_on_or_after})")
|
183
|
+
end
|
184
|
+
|
185
|
+
true
|
57
186
|
end
|
58
187
|
|
59
|
-
|
60
|
-
|
61
|
-
|
188
|
+
# Validates the Logout Request against the specified schema.
|
189
|
+
# @return [Boolean] True if the XML is valid, otherwise False if soft=True
|
190
|
+
# @raise [ValidationError] if soft == false and validation fails
|
191
|
+
#
|
192
|
+
def validate_structure
|
193
|
+
unless valid_saml?(document, soft)
|
194
|
+
return append_error("Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd")
|
62
195
|
end
|
196
|
+
|
197
|
+
true
|
198
|
+
end
|
199
|
+
|
200
|
+
# Validates that the Logout Request provided in the initialization is not empty,
|
201
|
+
# @return [Boolean] True if the required info is found, otherwise False if soft=True
|
202
|
+
# @raise [ValidationError] if soft == false and validation fails
|
203
|
+
#
|
204
|
+
def validate_request_state
|
205
|
+
return append_error("Blank logout request") if request.nil? || request.empty?
|
206
|
+
|
207
|
+
true
|
208
|
+
end
|
209
|
+
|
210
|
+
# Validates the Issuer of the Logout Request
|
211
|
+
# If fails, the error is added to the errors array
|
212
|
+
# @return [Boolean] True if the Issuer matchs the IdP entityId, otherwise False if soft=True
|
213
|
+
# @raise [ValidationError] if soft == false and validation fails
|
214
|
+
#
|
215
|
+
def validate_issuer
|
216
|
+
return true if settings.idp_entity_id.nil? || issuer.nil?
|
217
|
+
|
218
|
+
unless URI.parse(issuer) == URI.parse(settings.idp_entity_id)
|
219
|
+
return append_error("Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>")
|
220
|
+
end
|
221
|
+
|
222
|
+
true
|
223
|
+
end
|
224
|
+
|
225
|
+
# Validates the Signature if exists and GET parameters are provided
|
226
|
+
# @return [Boolean] True if not contains a Signature or if the Signature is valid, otherwise False if soft=True
|
227
|
+
# @raise [ValidationError] if soft == false and validation fails
|
228
|
+
#
|
229
|
+
def validate_signature
|
230
|
+
return true if options.nil?
|
231
|
+
return true unless options.has_key? :get_params
|
232
|
+
return true unless options[:get_params].has_key? 'Signature'
|
233
|
+
return true if settings.nil? || settings.get_idp_cert.nil?
|
234
|
+
|
235
|
+
query_string = OneLogin::RubySaml::Utils.build_query(
|
236
|
+
:type => 'SAMLRequest',
|
237
|
+
:data => options[:get_params]['SAMLRequest'],
|
238
|
+
:relay_state => options[:get_params]['RelayState'],
|
239
|
+
:sig_alg => options[:get_params]['SigAlg']
|
240
|
+
)
|
241
|
+
|
242
|
+
valid = OneLogin::RubySaml::Utils.verify_signature(
|
243
|
+
:cert => settings.get_idp_cert,
|
244
|
+
:sig_alg => options[:get_params]['SigAlg'],
|
245
|
+
:signature => options[:get_params]['Signature'],
|
246
|
+
:query_string => query_string
|
247
|
+
)
|
248
|
+
|
249
|
+
unless valid
|
250
|
+
return append_error("Invalid Signature on Logout Request")
|
251
|
+
end
|
252
|
+
|
63
253
|
true
|
64
254
|
end
|
65
255
|
|