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,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
|
+
@schema ||= 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
|