forked from SAML-Toolkits/ruby-saml
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlogoutresponse.rb
More file actions
262 lines (222 loc) · 9.36 KB
/
logoutresponse.rb
File metadata and controls
262 lines (222 loc) · 9.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
# frozen_string_literal: true
require "ruby_saml/xml"
require "ruby_saml/saml_message"
require "time"
module RubySaml
# SAML2 Logout Response (SLO IdP initiated, Parser)
class Logoutresponse < SamlMessage
include ErrorHandling
# RubySaml::Settings Toolkit settings
attr_accessor :settings
attr_reader :document
attr_reader :response
attr_reader :options
attr_accessor :soft
# Constructs the Logout Response. A Logout Response Object that is an extension of the SamlMessage class.
# @param response [String] A UUEncoded logout response from the IdP.
# @param settings [RubySaml::Settings|nil] Toolkit settings
# @param options [Hash] Extra parameters.
# :matches_request_id It will validate that the logout response matches the ID of the request.
# :get_params GET Parameters, including the SAMLResponse
# :relax_signature_validation to accept signatures if no idp certificate registered on settings
#
# @raise [ArgumentError] if response is nil
#
def initialize(response, settings = nil, options = {})
@errors = []
raise ArgumentError.new("Logoutresponse cannot be nil") if response.nil?
@settings = settings
if settings.nil? || settings.soft.nil?
@soft = true
else
@soft = settings.soft
end
@options = options
@response = RubySaml::XML::Decoder.decode_message(response, @settings&.message_max_bytesize)
@document = RubySaml::XML.safe_load_nokogiri(@response)
super()
end
def response_id
id(document)
end
# Checks if the Status has the "Success" code
# @return [Boolean] True if the StatusCode is Sucess
# @raise [ValidationError] if soft == false and validation fails
#
def success?
status_code == "urn:oasis:names:tc:SAML:2.0:status:Success"
end
# @return [String|nil] Gets the InResponseTo attribute from the Logout Response if exists.
#
def in_response_to
@in_response_to ||= document.at_xpath(
"/p:LogoutResponse",
{ "p" => RubySaml::XML::NS_PROTOCOL }
)&.[]('InResponseTo')
end
# @return [String] Gets the Issuer from the Logout Response.
#
def issuer
@issuer ||= document.at_xpath(
"/p:LogoutResponse/a:Issuer",
{ "p" => RubySaml::XML::NS_PROTOCOL, "a" => RubySaml::XML::NS_ASSERTION }
)&.text
end
# @return [String] Gets the StatusCode from a Logout Response.
#
def status_code
@status_code ||= document.at_xpath(
"/p:LogoutResponse/p:Status/p:StatusCode",
{ "p" => RubySaml::XML::NS_PROTOCOL }
)&.[]('Value')
end
def status_message
@status_message ||= document.at_xpath(
"/p:LogoutResponse/p:Status/p:StatusMessage",
{ "p" => RubySaml::XML::NS_PROTOCOL }
)&.text
end
# Aux function to validate the Logout Response
# @param collect_errors [Boolean] Stop validation when first error appears or keep validating. (if soft=true)
# @return [Boolean] TRUE if the SAML Response is valid
# @raise [ValidationError] if soft == false and validation fails
#
def validate(collect_errors = false)
reset_errors!
validations = %i[
valid_state?
validate_success_status
validate_structure
valid_in_response_to?
valid_issuer?
validate_signature
]
if collect_errors
validations.each { |validation| send(validation) }
@errors.empty?
else
validations.all? { |validation| send(validation) }
end
end
private
# Validates the Status of the Logout Response
# If fails, the error is added to the errors array, including the StatusCode returned and the Status Message.
# @return [Boolean] True if the Logout Response contains a Success code, otherwise False if soft=True
# @raise [ValidationError] if soft == false and validation fails
#
def validate_success_status
return true if success?
error_msg = 'The status code of the Logout Response was not Success'
status_error_msg = RubySaml::Utils.status_error_msg(error_msg, status_code, status_message)
append_error(status_error_msg)
end
# Validates the Logout Response against the specified schema.
# @return [Boolean] True if the XML is valid, otherwise False if soft=True
# @raise [ValidationError] if soft == false and validation fails
#
def validate_structure
check_malformed_doc = check_malformed_doc?(settings)
unless valid_saml?(document, soft, check_malformed_doc: check_malformed_doc)
return append_error("Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd")
end
true
end
# Validates that the Logout Response provided in the initialization is not empty,
# also check that the setting and the IdP cert were also provided
# @return [Boolean] True if the required info is found, otherwise False if soft=True
# @raise [ValidationError] if soft == false and validation fails
#
def valid_state?
return append_error("Blank logout response") if response.empty?
return append_error("No settings on logout response") if settings.nil?
return append_error("No sp_entity_id in settings of the logout response") if settings.sp_entity_id.nil?
if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil? && settings.idp_cert_multi.nil?
return append_error("No fingerprint or certificate on settings of the logout response")
end
true
end
# Validates if a provided :matches_request_id matchs the inResponseTo value.
# @param soft [String|nil] request_id The ID of the Logout Request sent by this SP to the IdP (if was sent any)
# @return [Boolean] True if there is no request_id or it match, otherwise False if soft=True
# @raise [ValidationError] if soft == false and validation fails
#
def valid_in_response_to?
return true unless options.key? :matches_request_id
return true if options[:matches_request_id].nil?
return true unless options[:matches_request_id] != in_response_to
error_msg = "The InResponseTo of the Logout Response: #{in_response_to}, does not match the ID of the Logout Request sent by the SP: #{options[:matches_request_id]}"
append_error(error_msg)
end
# Validates the Issuer of the Logout Response
# @return [Boolean] True if the Issuer matchs the IdP entityId, otherwise False if soft=True
# @raise [ValidationError] if soft == false and validation fails
#
def valid_issuer?
return true if settings.idp_entity_id.nil? || issuer.nil?
unless RubySaml::Utils.uri_match?(issuer, settings.idp_entity_id)
return append_error("Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>")
end
true
end
# Validates the Signature if it exists and the GET parameters are provided
# @return [Boolean] True if not contains a Signature or if the Signature is valid, otherwise False if soft=True
# @raise [ValidationError] if soft == false and validation fails
#
def validate_signature
return true if options.nil?
return true unless options.key? :get_params
return true unless options[:get_params].key? 'Signature'
options[:raw_get_params] = RubySaml::Utils.prepare_raw_get_params(options[:raw_get_params], options[:get_params], settings.security[:lowercase_url_encoding])
if options[:get_params]['SigAlg'].nil? && !options[:raw_get_params]['SigAlg'].nil?
options[:get_params]['SigAlg'] = CGI.unescape(options[:raw_get_params]['SigAlg'])
end
idp_cert = settings.get_idp_cert
idp_certs = settings.get_idp_cert_multi
if idp_cert.nil? && (idp_certs.nil? || idp_certs[:signing].empty?)
return options.key? :relax_signature_validation
end
query_string = RubySaml::Utils.build_query_from_raw_parts(
type: 'SAMLResponse',
raw_data: options[:raw_get_params]['SAMLResponse'],
raw_relay_state: options[:raw_get_params]['RelayState'],
raw_sig_alg: options[:raw_get_params]['SigAlg']
)
expired = false
if idp_certs.nil? || idp_certs[:signing].empty?
valid = RubySaml::Utils.verify_signature(
cert: idp_cert,
sig_alg: options[:get_params]['SigAlg'],
signature: options[:get_params]['Signature'],
query_string: query_string
)
if valid && settings.security[:check_idp_cert_expiration] && RubySaml::Utils.is_cert_expired(idp_cert)
expired = true
end
else
valid = false
idp_certs[:signing].each do |signing_idp_cert|
valid = RubySaml::Utils.verify_signature(
cert: signing_idp_cert,
sig_alg: options[:get_params]['SigAlg'],
signature: options[:get_params]['Signature'],
query_string: query_string
)
next unless valid
if settings.security[:check_idp_cert_expiration] && RubySaml::Utils.is_cert_expired(signing_idp_cert)
expired = true
end
break
end
end
if expired
error_msg = "IdP x509 certificate expired"
return append_error(error_msg)
end
unless valid
error_msg = "Invalid Signature on Logout Response"
return append_error(error_msg)
end
true
end
end
end