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,158 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'zlib'
|
3
|
+
require 'base64'
|
4
|
+
require 'nokogiri'
|
5
|
+
require 'rexml/document'
|
6
|
+
require 'rexml/xpath'
|
7
|
+
require 'thread'
|
8
|
+
|
9
|
+
# Only supports SAML 2.0
|
10
|
+
module OneLogin
|
11
|
+
module RubySaml
|
12
|
+
|
13
|
+
# SAML2 Message
|
14
|
+
#
|
15
|
+
class SamlMessage
|
16
|
+
include REXML
|
17
|
+
|
18
|
+
ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
|
19
|
+
PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
|
20
|
+
|
21
|
+
BASE64_FORMAT = %r(\A[A-Za-z0-9+/]{4}*[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=?\Z)
|
22
|
+
|
23
|
+
# @return [Nokogiri::XML::Schema] Gets the schema object of the SAML 2.0 Protocol schema
|
24
|
+
#
|
25
|
+
def self.schema
|
26
|
+
Mutex.new.synchronize do
|
27
|
+
Dir.chdir(File.expand_path("../../../schemas", __FILE__)) do
|
28
|
+
::Nokogiri::XML::Schema(File.read("saml-schema-protocol-2.0.xsd"))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
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
|
+
#
|
65
|
+
def valid_saml?(document, soft = true)
|
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
|
74
|
+
|
75
|
+
SamlMessage.schema.validate(xml).map do |error|
|
76
|
+
return false if soft
|
77
|
+
validation_error("#{error.message}\n\n#{xml.to_s}")
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Raise a ValidationError with the provided message
|
82
|
+
# @param message [String] Message of the exception
|
83
|
+
# @raise [ValidationError]
|
84
|
+
#
|
85
|
+
def validation_error(message)
|
86
|
+
raise ValidationError.new(message)
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
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
|
94
|
+
#
|
95
|
+
def decode_raw_saml(saml)
|
96
|
+
return saml unless base64_encoded?(saml)
|
97
|
+
|
98
|
+
decoded = decode(saml)
|
99
|
+
begin
|
100
|
+
inflate(decoded)
|
101
|
+
rescue
|
102
|
+
decoded
|
103
|
+
end
|
104
|
+
end
|
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
|
+
#
|
111
|
+
def encode_raw_saml(saml, settings)
|
112
|
+
saml = deflate(saml) if settings.compress_request
|
113
|
+
|
114
|
+
CGI.escape(Base64.encode64(saml))
|
115
|
+
end
|
116
|
+
|
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)
|
123
|
+
end
|
124
|
+
|
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/, "")
|
131
|
+
end
|
132
|
+
|
133
|
+
# Check if a string is base64 encoded
|
134
|
+
# @param string [String] string to check the encoding of
|
135
|
+
# @return [true, false] whether or not the string is base64 encoded
|
136
|
+
#
|
137
|
+
def base64_encoded?(string)
|
138
|
+
!!string.gsub(/[\r\n]|\\r|\\n/, "").match(BASE64_FORMAT)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Inflate method
|
142
|
+
# @param deflated [String] The string
|
143
|
+
# @return [String] The inflated string
|
144
|
+
#
|
145
|
+
def inflate(deflated)
|
146
|
+
Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(deflated)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Deflate method
|
150
|
+
# @param inflated [String] The string
|
151
|
+
# @return [String] The deflated string
|
152
|
+
#
|
153
|
+
def deflate(inflated)
|
154
|
+
Zlib::Deflate.deflate(inflated, 9)[2..-5]
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
require "xml_security"
|
2
|
+
require "onelogin/ruby-saml/attribute_service"
|
3
|
+
require "onelogin/ruby-saml/utils"
|
4
|
+
|
5
|
+
# Only supports SAML 2.0
|
6
|
+
module OneLogin
|
7
|
+
module RubySaml
|
8
|
+
|
9
|
+
# SAML2 Toolkit Settings
|
10
|
+
#
|
11
|
+
class Settings
|
12
|
+
def initialize(overrides = {})
|
13
|
+
config = DEFAULTS.merge(overrides)
|
14
|
+
config.each do |k,v|
|
15
|
+
acc = "#{k.to_s}=".to_sym
|
16
|
+
if respond_to? acc
|
17
|
+
value = v.is_a?(Hash) ? v.dup : v
|
18
|
+
send(acc, value)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
@attribute_consuming_service = AttributeService.new
|
22
|
+
end
|
23
|
+
|
24
|
+
# IdP Data
|
25
|
+
attr_accessor :idp_entity_id
|
26
|
+
attr_accessor :idp_sso_target_url
|
27
|
+
attr_accessor :idp_slo_target_url
|
28
|
+
attr_accessor :idp_cert
|
29
|
+
attr_accessor :idp_cert_fingerprint
|
30
|
+
attr_accessor :idp_cert_fingerprint_algorithm
|
31
|
+
# SP Data
|
32
|
+
attr_accessor :issuer
|
33
|
+
attr_accessor :assertion_consumer_service_url
|
34
|
+
attr_accessor :assertion_consumer_service_binding
|
35
|
+
attr_accessor :sp_name_qualifier
|
36
|
+
attr_accessor :name_identifier_format
|
37
|
+
attr_accessor :name_identifier_value
|
38
|
+
attr_accessor :sessionindex
|
39
|
+
attr_accessor :compress_request
|
40
|
+
attr_accessor :compress_response
|
41
|
+
attr_accessor :double_quote_xml_attribute_values
|
42
|
+
attr_accessor :passive
|
43
|
+
attr_accessor :protocol_binding
|
44
|
+
attr_accessor :attributes_index
|
45
|
+
attr_accessor :force_authn
|
46
|
+
attr_accessor :certificate
|
47
|
+
attr_accessor :private_key
|
48
|
+
attr_accessor :authn_context
|
49
|
+
attr_accessor :authn_context_comparison
|
50
|
+
attr_accessor :authn_context_decl_ref
|
51
|
+
attr_reader :attribute_consuming_service
|
52
|
+
# Work-flow
|
53
|
+
attr_accessor :security
|
54
|
+
attr_accessor :soft
|
55
|
+
# Compability
|
56
|
+
attr_accessor :assertion_consumer_logout_service_url
|
57
|
+
attr_accessor :assertion_consumer_logout_service_binding
|
58
|
+
|
59
|
+
# @return [String] Single Logout Service URL.
|
60
|
+
#
|
61
|
+
def single_logout_service_url
|
62
|
+
val = nil
|
63
|
+
if @single_logout_service_url.nil?
|
64
|
+
if @assertion_consumer_logout_service_url
|
65
|
+
val = @assertion_consumer_logout_service_url
|
66
|
+
end
|
67
|
+
else
|
68
|
+
val = @single_logout_service_url
|
69
|
+
end
|
70
|
+
val
|
71
|
+
end
|
72
|
+
|
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
|
78
|
+
end
|
79
|
+
|
80
|
+
# @return [String] Single Logout Service Binding.
|
81
|
+
#
|
82
|
+
def single_logout_service_binding
|
83
|
+
val = nil
|
84
|
+
if @single_logout_service_binding.nil?
|
85
|
+
if @assertion_consumer_logout_service_binding
|
86
|
+
val = @assertion_consumer_logout_service_binding
|
87
|
+
end
|
88
|
+
else
|
89
|
+
val = @single_logout_service_binding
|
90
|
+
end
|
91
|
+
val
|
92
|
+
end
|
93
|
+
|
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
|
101
|
+
end
|
102
|
+
|
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
|
113
|
+
end
|
114
|
+
end
|
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
|
+
#
|
136
|
+
def get_sp_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)
|
141
|
+
end
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
DEFAULTS = {
|
146
|
+
:assertion_consumer_service_binding => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST".freeze,
|
147
|
+
:single_logout_service_binding => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect".freeze,
|
148
|
+
:idp_cert_fingerprint_algorithm => XMLSecurity::Document::SHA1,
|
149
|
+
:compress_request => true,
|
150
|
+
:compress_response => true,
|
151
|
+
:soft => true,
|
152
|
+
:security => {
|
153
|
+
:authn_requests_signed => false,
|
154
|
+
:logout_requests_signed => false,
|
155
|
+
:logout_responses_signed => false,
|
156
|
+
:metadata_signed => false,
|
157
|
+
:embed_sign => false,
|
158
|
+
:digest_method => XMLSecurity::Document::SHA1,
|
159
|
+
:signature_method => XMLSecurity::Document::RSA_SHA1
|
160
|
+
}.freeze,
|
161
|
+
:double_quote_xml_attribute_values => false,
|
162
|
+
}.freeze
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,258 @@
|
|
1
|
+
require 'zlib'
|
2
|
+
require 'time'
|
3
|
+
require 'nokogiri'
|
4
|
+
|
5
|
+
require "onelogin/ruby-saml/saml_message"
|
6
|
+
|
7
|
+
# Only supports SAML 2.0
|
8
|
+
module OneLogin
|
9
|
+
module RubySaml
|
10
|
+
|
11
|
+
# SAML2 Logout Request (SLO IdP initiated, Parser)
|
12
|
+
#
|
13
|
+
class SloLogoutrequest < SamlMessage
|
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
|
+
|
21
|
+
attr_reader :document
|
22
|
+
attr_reader :request
|
23
|
+
attr_reader :options
|
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
|
+
#
|
34
|
+
def initialize(request, options = {})
|
35
|
+
@errors = []
|
36
|
+
raise ArgumentError.new("Request cannot be nil") if request.nil?
|
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
|
+
|
47
|
+
@request = decode_raw_saml(request)
|
48
|
+
@document = REXML::Document.new(@request)
|
49
|
+
end
|
50
|
+
|
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)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Reset the errors array
|
59
|
+
def reset_errors!
|
60
|
+
@errors = []
|
61
|
+
end
|
62
|
+
|
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
|
+
#
|
72
|
+
def name_id
|
73
|
+
@name_id ||= begin
|
74
|
+
node = REXML::XPath.first(document, "/p:LogoutRequest/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION })
|
75
|
+
node.nil? ? nil : node.text
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
alias_method :nameid, :name_id
|
80
|
+
|
81
|
+
# @return [String|nil] Gets the ID attribute from the Logout Request. if exists.
|
82
|
+
#
|
83
|
+
def id
|
84
|
+
super(document)
|
85
|
+
end
|
86
|
+
|
87
|
+
# @return [String] Gets the Issuer from the Logout Request.
|
88
|
+
#
|
89
|
+
def issuer
|
90
|
+
@issuer ||= begin
|
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
|
112
|
+
end
|
113
|
+
end
|
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
|
+
|
132
|
+
private
|
133
|
+
|
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
|
186
|
+
end
|
187
|
+
|
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")
|
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
|
+
|
253
|
+
true
|
254
|
+
end
|
255
|
+
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|