omniauth-saml 1.1.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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