omniauth-saml 1.1.0 → 2.1.0

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.

Potentially problematic release.


This version of omniauth-saml might be problematic. Click here for more details.

@@ -6,60 +6,293 @@ module OmniAuth
6
6
  class SAML
7
7
  include OmniAuth::Strategy
8
8
 
9
+ def self.inherited(subclass)
10
+ OmniAuth::Strategy.included(subclass)
11
+ end
12
+
13
+ RUBYSAML_RESPONSE_OPTIONS = OneLogin::RubySaml::Response::AVAILABLE_OPTIONS
14
+
9
15
  option :name_identifier_format, nil
10
- option :idp_sso_target_url_runtime_params, {}
16
+ option :idp_sso_service_url_runtime_params, {}
17
+ option :request_attributes, [
18
+ { :name => 'email', :name_format => 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', :friendly_name => 'Email address' },
19
+ { :name => 'name', :name_format => 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', :friendly_name => 'Full name' },
20
+ { :name => 'first_name', :name_format => 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', :friendly_name => 'Given name' },
21
+ { :name => 'last_name', :name_format => 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', :friendly_name => 'Family name' }
22
+ ]
23
+ option :attribute_service_name, 'Required attributes'
24
+ option :attribute_statements, {
25
+ name: ["name"],
26
+ email: ["email", "mail"],
27
+ first_name: ["first_name", "firstname", "firstName"],
28
+ last_name: ["last_name", "lastname", "lastName"]
29
+ }
30
+ option :slo_default_relay_state
31
+ option :uid_attribute
32
+ option :idp_slo_session_destroy, proc { |_env, session| session.clear }
11
33
 
12
34
  def request_phase
13
- options[:assertion_consumer_service_url] ||= callback_url
14
- runtime_request_parameters = options.delete(:idp_sso_target_url_runtime_params)
35
+ authn_request = OneLogin::RubySaml::Authrequest.new
36
+
37
+ with_settings do |settings|
38
+ redirect(authn_request.create(settings, additional_params_for_authn_request))
39
+ end
40
+ end
41
+
42
+ def callback_phase
43
+ raise OmniAuth::Strategies::SAML::ValidationError.new("SAML response missing") unless request.params["SAMLResponse"]
44
+
45
+ with_settings do |settings|
46
+ # Call a fingerprint validation method if there's one
47
+ validate_fingerprint(settings) if options.idp_cert_fingerprint_validator
48
+
49
+ handle_response(request.params["SAMLResponse"], options_for_response_object, settings) do
50
+ super
51
+ end
52
+ end
53
+ rescue OmniAuth::Strategies::SAML::ValidationError
54
+ fail!(:invalid_ticket, $!)
55
+ rescue OneLogin::RubySaml::ValidationError
56
+ fail!(:invalid_ticket, $!)
57
+ end
15
58
 
16
- additional_params = {}
17
- runtime_request_parameters.each_pair do |request_param_key, mapped_param_key|
18
- additional_params[mapped_param_key] = request.params[request_param_key.to_s] if request.params.has_key?(request_param_key.to_s)
19
- end if runtime_request_parameters
59
+ # Obtain an idp certificate fingerprint from the response.
60
+ def response_fingerprint
61
+ response = request.params["SAMLResponse"]
62
+ response = (response =~ /^</) ? response : Base64.decode64(response)
63
+ document = XMLSecurity::SignedDocument::new(response)
64
+ cert_element = REXML::XPath.first(document, "//ds:X509Certificate", { "ds"=> 'http://www.w3.org/2000/09/xmldsig#' })
65
+ base64_cert = cert_element.text
66
+ cert_text = Base64.decode64(base64_cert)
67
+ cert = OpenSSL::X509::Certificate.new(cert_text)
68
+ Digest::SHA1.hexdigest(cert.to_der).upcase.scan(/../).join(':')
69
+ end
20
70
 
21
- authn_request = Onelogin::Saml::Authrequest.new
22
- settings = Onelogin::Saml::Settings.new(options)
71
+ def other_phase
72
+ if request_path_pattern.match(current_path)
73
+ @env['omniauth.strategy'] ||= self
74
+ setup_phase
23
75
 
24
- redirect(authn_request.create(settings, additional_params))
76
+ if on_subpath?(:metadata)
77
+ other_phase_for_metadata
78
+ elsif on_subpath?(:slo)
79
+ other_phase_for_slo
80
+ elsif on_subpath?(:spslo)
81
+ other_phase_for_spslo
82
+ else
83
+ call_app!
84
+ end
85
+ else
86
+ call_app!
87
+ end
25
88
  end
26
89
 
27
- def callback_phase
28
- unless request.params['SAMLResponse']
29
- raise OmniAuth::Strategies::SAML::ValidationError.new("SAML response missing")
90
+ uid do
91
+ if options.uid_attribute
92
+ ret = find_attribute_by([options.uid_attribute])
93
+ if ret.nil?
94
+ raise OmniAuth::Strategies::SAML::ValidationError.new("SAML response missing '#{options.uid_attribute}' attribute")
95
+ end
96
+ ret
97
+ else
98
+ @name_id
30
99
  end
100
+ end
31
101
 
32
- response = Onelogin::Saml::Response.new(request.params['SAMLResponse'], options)
33
- response.settings = Onelogin::Saml::Settings.new(options)
102
+ info do
103
+ found_attributes = options.attribute_statements.map do |key, values|
104
+ attribute = find_attribute_by(values)
105
+ [key, attribute]
106
+ end
34
107
 
108
+ Hash[found_attributes]
109
+ end
110
+
111
+ extra { { :raw_info => @attributes, :session_index => @session_index, :response_object => @response_object } }
112
+
113
+ def find_attribute_by(keys)
114
+ keys.each do |key|
115
+ return @attributes[key] if @attributes[key]
116
+ end
117
+
118
+ nil
119
+ end
120
+
121
+ private
122
+
123
+ def request_path_pattern
124
+ @request_path_pattern ||= %r{\A#{Regexp.quote(request_path)}(/|\z)}
125
+ end
126
+
127
+ def on_subpath?(subpath)
128
+ on_path?("#{request_path}/#{subpath}")
129
+ end
130
+
131
+ def handle_response(raw_response, opts, settings)
132
+ response = OneLogin::RubySaml::Response.new(raw_response, opts.merge(settings: settings))
133
+ response.attributes["fingerprint"] = settings.idp_cert_fingerprint
134
+ response.soft = false
135
+
136
+ response.is_valid?
35
137
  @name_id = response.name_id
138
+ @session_index = response.sessionindex
36
139
  @attributes = response.attributes
140
+ @response_object = response
37
141
 
38
- if @name_id.nil? || @name_id.empty?
39
- raise OmniAuth::Strategies::SAML::ValidationError.new("SAML response missing 'name_id'")
142
+ session["saml_uid"] = @name_id
143
+ session["saml_session_index"] = @session_index
144
+ yield
145
+ end
146
+
147
+ def slo_relay_state
148
+ if request.params.has_key?("RelayState") && request.params["RelayState"] != ""
149
+ request.params["RelayState"]
150
+ else
151
+ slo_default_relay_state = options.slo_default_relay_state
152
+ if slo_default_relay_state.respond_to?(:call)
153
+ if slo_default_relay_state.arity == 1
154
+ slo_default_relay_state.call(request)
155
+ else
156
+ slo_default_relay_state.call
157
+ end
158
+ else
159
+ slo_default_relay_state
160
+ end
40
161
  end
162
+ end
41
163
 
42
- response.validate!
164
+ def handle_logout_response(raw_response, settings)
165
+ # After sending an SP initiated LogoutRequest to the IdP, we need to accept
166
+ # the LogoutResponse, verify it, then actually delete our session.
43
167
 
44
- super
45
- rescue OmniAuth::Strategies::SAML::ValidationError
46
- fail!(:invalid_ticket, $!)
47
- rescue Onelogin::Saml::ValidationError
48
- fail!(:invalid_ticket, $!)
168
+ logout_response = OneLogin::RubySaml::Logoutresponse.new(raw_response, settings, :matches_request_id => session["saml_transaction_id"])
169
+ logout_response.soft = false
170
+ logout_response.validate
171
+
172
+ session.delete("saml_uid")
173
+ session.delete("saml_transaction_id")
174
+ session.delete("saml_session_index")
175
+
176
+ redirect(slo_relay_state)
49
177
  end
50
178
 
51
- uid { @name_id }
179
+ def handle_logout_request(raw_request, settings)
180
+ logout_request = OneLogin::RubySaml::SloLogoutrequest.new(raw_request, {}.merge(settings: settings).merge(get_params: @request.params))
52
181
 
53
- info do
54
- {
55
- :name => @attributes[:name],
56
- :email => @attributes[:email] || @attributes[:mail],
57
- :first_name => @attributes[:first_name] || @attributes[:firstname] || @attributes[:firstName],
58
- :last_name => @attributes[:last_name] || @attributes[:lastname] || @attributes[:lastName]
59
- }
182
+ if logout_request.is_valid? &&
183
+ logout_request.name_id == session["saml_uid"]
184
+
185
+ # Actually log out this session
186
+ options[:idp_slo_session_destroy].call @env, session
187
+
188
+ # Generate a response to the IdP.
189
+ logout_request_id = logout_request.id
190
+ logout_response = OneLogin::RubySaml::SloLogoutresponse.new.create(settings, logout_request_id, nil, RelayState: slo_relay_state)
191
+ redirect(logout_response)
192
+ else
193
+ raise OmniAuth::Strategies::SAML::ValidationError.new("SAML failed to process LogoutRequest")
194
+ end
195
+ end
196
+
197
+ # Create a SP initiated SLO: https://github.com/onelogin/ruby-saml#single-log-out
198
+ def generate_logout_request(settings)
199
+ logout_request = OneLogin::RubySaml::Logoutrequest.new()
200
+
201
+ # Since we created a new SAML request, save the transaction_id
202
+ # to compare it with the response we get back
203
+ session["saml_transaction_id"] = logout_request.uuid
204
+
205
+ if settings.name_identifier_value.nil?
206
+ settings.name_identifier_value = session["saml_uid"]
207
+ end
208
+
209
+ if settings.sessionindex.nil?
210
+ settings.sessionindex = session["saml_session_index"]
211
+ end
212
+
213
+ logout_request.create(settings, RelayState: slo_relay_state)
214
+ end
215
+
216
+ def with_settings
217
+ options[:assertion_consumer_service_url] ||= callback_url
218
+ yield OneLogin::RubySaml::Settings.new(options)
219
+ end
220
+
221
+ def validate_fingerprint(settings)
222
+ fingerprint_exists = options.idp_cert_fingerprint_validator[response_fingerprint]
223
+
224
+ unless fingerprint_exists
225
+ raise OmniAuth::Strategies::SAML::ValidationError.new("Non-existent fingerprint")
226
+ end
227
+
228
+ # id_cert_fingerprint becomes the given fingerprint if it exists
229
+ settings.idp_cert_fingerprint = fingerprint_exists
230
+ end
231
+
232
+ def options_for_response_object
233
+ # filter options to select only extra parameters
234
+ opts = options.select {|k,_| RUBYSAML_RESPONSE_OPTIONS.include?(k.to_sym)}
235
+
236
+ # symbolize keys without activeSupport/symbolize_keys (ruby-saml use symbols)
237
+ opts.inject({}) do |new_hash, (key, value)|
238
+ new_hash[key.to_sym] = value
239
+ new_hash
240
+ end
241
+ end
242
+
243
+ def other_phase_for_metadata
244
+ with_settings do |settings|
245
+ # omniauth does not set the strategy on the other_phase
246
+ response = OneLogin::RubySaml::Metadata.new
247
+
248
+ add_request_attributes_to(settings) if options.request_attributes.length > 0
249
+
250
+ Rack::Response.new(response.generate(settings), 200, { "Content-Type" => "application/xml" }).finish
251
+ end
252
+ end
253
+
254
+ def other_phase_for_slo
255
+ with_settings do |settings|
256
+ if request.params["SAMLResponse"]
257
+ handle_logout_response(request.params["SAMLResponse"], settings)
258
+ elsif request.params["SAMLRequest"]
259
+ handle_logout_request(request.params["SAMLRequest"], settings)
260
+ else
261
+ raise OmniAuth::Strategies::SAML::ValidationError.new("SAML logout response/request missing")
262
+ end
263
+ end
60
264
  end
61
265
 
62
- extra { { :raw_info => @attributes } }
266
+ def other_phase_for_spslo
267
+ if options.idp_slo_service_url
268
+ with_settings do |settings|
269
+ redirect(generate_logout_request(settings))
270
+ end
271
+ else
272
+ Rack::Response.new("Not Implemented", 501, { "Content-Type" => "text/html" }).finish
273
+ end
274
+ end
275
+
276
+ def add_request_attributes_to(settings)
277
+ settings.attribute_consuming_service.service_name options.attribute_service_name
278
+ settings.sp_entity_id = options.sp_entity_id
279
+
280
+ options.request_attributes.each do |attribute|
281
+ settings.attribute_consuming_service.add_attribute attribute
282
+ end
283
+ end
284
+
285
+ def additional_params_for_authn_request
286
+ {}.tap do |additional_params|
287
+ runtime_request_parameters = options.delete(:idp_sso_service_url_runtime_params)
288
+
289
+ if runtime_request_parameters
290
+ runtime_request_parameters.each_pair do |request_param_key, mapped_param_key|
291
+ additional_params[mapped_param_key] = request.params[request_param_key.to_s] if request.params.has_key?(request_param_key.to_s)
292
+ end
293
+ end
294
+ end
295
+ end
63
296
  end
64
297
  end
65
298
  end
@@ -1,5 +1,5 @@
1
1
  module OmniAuth
2
2
  module SAML
3
- VERSION = '1.1.0'
3
+ VERSION = '2.1.0'
4
4
  end
5
5
  end