/
strategy.rb
362 lines (306 loc) · 10.7 KB
/
strategy.rb
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
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
require 'omniauth'
require 'hashie/mash'
module OmniAuth
class NoSessionError < StandardError; end
# The Strategy is the base unit of OmniAuth's ability to
# wrangle multiple providers. Each strategy provided by
# OmniAuth includes this mixin to gain the default functionality
# necessary to be compatible with the OmniAuth library.
module Strategy
def self.included(base)
OmniAuth.strategies << base
base.class_eval do
attr_reader :app, :env, :options, :response
end
base.extend ClassMethods
end
module ClassMethods
# Returns an inherited set of default options set at the class-level
# for each strategy.
def default_options
return @default_options if @default_options
existing = superclass.respond_to?(:default_options) ? superclass.default_options : {}
@default_options = OmniAuth::Strategy::Options.new(existing)
end
# This allows for more declarative subclassing of strategies by allowing
# default options to be set using a simple configure call.
#
# @param options [Hash] If supplied, these will be the default options (deep-merged into the superclass's default options).
# @yield [Options] The options Mash that allows you to set your defaults as you'd like.
#
# @example Using a yield to configure the default options.
#
# class MyStrategy
# include OmniAuth::Strategy
#
# configure do |c|
# c.foo = 'bar'
# end
# end
#
# @example Using a hash to configure the default options.
#
# class MyStrategy
# include OmniAuth::Strategy
# configure foo: 'bar'
# end
def configure(options = nil)
yield default_options and return unless options
default_options.deep_merge!(options)
end
# Directly declare a default option for your class. This is a useful from
# a documentation perspective as it provides a simple line-by-line analysis
# of the kinds of options your strategy provides by default.
#
# @param name [Symbol] The key of the default option in your configuration hash.
# @param value [Object] The value your object defaults to. Nil if not provided.
#
# @example
#
# class MyStrategy
# include OmniAuth::Strategy
#
# option :foo, 'bar'
# option
# end
def option(name, value = nil)
default_options[name] = value
end
# Sets (and retrieves) option key names for initializer arguments to be
# recorded as. This takes care of 90% of the use cases for overriding
# the initializer in OmniAuth Strategies.
def args(args = nil)
return @args || [] unless args
@args = Array(args)
end
end
# Initializes the strategy by passing in the Rack endpoint,
# the unique URL segment name for this strategy, and any
# additional arguments. An `options` hash is automatically
# created from the last argument if it is a hash.
#
# @param app [Rack application] The application on which this middleware is applied.
#
# @overload new(app, options = {})
# If nothing but a hash is supplied, initialized with the supplied options
# overriding the strategy's default options via a deep merge.
# @overload new(app, *args, options = {})
# If the strategy has supplied custom arguments that it accepts, they may
# will be passed through and set to the appropriate values.
#
# @yield [Options] Yields options to block for further configuration.
def initialize(app, *args, &block)
@app = app
@options = self.class.default_options.dup
options.deep_merge!(args.pop) if args.last.is_a?(Hash)
options.name ||= self.class.to_s.split('::').last.downcase
self.class.args.each do |arg|
options[arg] = args.shift
end
# Make sure that all of the args have been dealt with, otherwise error out.
raise ArgumentError, "Received wrong number of arguments. #{args.inspect}" unless args.empty?
yield options if block_given?
end
def inspect
"#<#{self.class.to_s}>"
end
# Duplicates this instance and runs #call! on it.
# @param [Hash] The Rack environment.
def call(env)
dup.call!(env)
end
# The logic for dispatching any additional actions that need
# to be taken. For instance, calling the request phase if
# the request path is recognized.
#
# @param env [Hash] The Rack environment.
def call!(env)
raise OmniAuth::NoSessionError.new("You must provide a session to use OmniAuth.") unless env['rack.session']
@env = env
@env['omniauth.strategy'] = self if on_auth_path?
return mock_call!(env) if OmniAuth.config.test_mode
return options_call if on_auth_path? && options_request?
return request_call if on_request_path? && OmniAuth.config.allowed_request_methods.include?(request.request_method.downcase.to_sym)
return callback_call if on_callback_path?
return other_phase if respond_to?(:other_phase)
@app.call(env)
end
# Responds to an OPTIONS request.
def options_call
verbs = OmniAuth.config.allowed_request_methods.map(&:to_s).map(&:upcase).join(', ')
return [ 200, { 'Allow' => verbs }, [] ]
end
# Performs the steps necessary to run the request phase of a strategy.
def request_call
setup_phase
if response = call_through_to_app
response
else
if request.params['origin']
@env['rack.session']['omniauth.origin'] = request.params['origin']
elsif env['HTTP_REFERER'] && !env['HTTP_REFERER'].match(/#{request_path}$/)
@env['rack.session']['omniauth.origin'] = env['HTTP_REFERER']
end
request_phase
end
end
# Performs the steps necessary to run the callback phase of a strategy.
def callback_call
setup_phase
@env['omniauth.origin'] = session.delete('omniauth.origin')
@env['omniauth.origin'] = nil if env['omniauth.origin'] == ''
@env['omniauth.params'] = session.delete('query_params') || {}
callback_phase
end
# Returns true if the environment recognizes either the
# request or callback path.
def on_auth_path?
on_request_path? || on_callback_path?
end
def on_request_path?
on_path?(request_path)
end
def on_callback_path?
on_path?(callback_path)
end
def on_path?(path)
current_path.casecmp(path) == 0
end
def options_request?
request.request_method == 'OPTIONS'
end
# This is called in lieu of the normal request process
# in the event that OmniAuth has been configured to be
# in test mode.
def mock_call!(env)
return mock_request_call if on_request_path?
return mock_callback_call if on_callback_path?
call_app!
end
def mock_request_call
setup_phase
return response if response = call_through_to_app
if request.params['origin']
@env['rack.session']['omniauth.origin'] = request.params['origin']
elsif env['HTTP_REFERER'] && !env['HTTP_REFERER'].match(/#{request_path}$/)
@env['rack.session']['omniauth.origin'] = env['HTTP_REFERER']
end
redirect(script_name + callback_path + query_string)
end
def mock_callback_call
setup_phase
mocked_auth = OmniAuth.mock_auth_for(name.to_s)
if mocked_auth.is_a?(Symbol)
fail!(mocked_auth)
else
@env['omniauth.auth'] = mocked_auth
@env['omniauth.origin'] = session.delete('omniauth.origin')
@env['omniauth.origin'] = nil if env['omniauth.origin'] == ''
call_app!
end
end
# The setup phase looks for the `:setup` option to exist and,
# if it is, will call either the Rack endpoint supplied to the
# `:setup` option or it will call out to the setup path of the
# underlying application. This will default to `/auth/:provider/setup`.
def setup_phase
if options[:setup].respond_to?(:call)
options[:setup].call(env)
elsif options.setup?
setup_env = env.merge('PATH_INFO' => setup_path, 'REQUEST_METHOD' => 'GET')
call_app!(setup_env)
end
end
def request_phase
raise NotImplementedError
end
def uid
raise NotImplementedError
end
def info
raise NotImplementedError
end
def auth_hash
hash = AuthHash.new(:provider => name, :uid => uid)
hash.info = info unless options.skip_info?
hash
end
def callback_phase
self.env['omniauth.auth'] = auth_hash
call_app!
end
def path_prefix
options[:path_prefix] || OmniAuth.config.path_prefix
end
def request_path
options[:request_path] || "#{path_prefix}/#{name}"
end
def callback_path
options[:callback_path] || "#{path_prefix}/#{name}/callback"
end
def setup_path
options[:setup_path] || "#{path_prefix}/#{name}/setup"
end
def current_path
request.path_info.downcase.sub(/\/$/,'')
end
def query_string
request.query_string.empty? ? "" : "?#{request.query_string}"
end
def call_through_to_app
status, headers, body = *call_app!
session['query_params'] = Rack::Request.new(env).params
@response = Rack::Response.new(body, status, headers)
status == 404 ? nil : @response.finish
end
def call_app!(env = @env)
@app.call(env)
end
def full_host
case OmniAuth.config.full_host
when String
OmniAuth.config.full_host
when Proc
OmniAuth.config.full_host.call(env)
else
uri = URI.parse(request.url.gsub(/\?.*$/,''))
uri.path = ''
uri.query = nil
uri.to_s
end
end
def callback_url
full_host + script_name + callback_path + query_string
end
def script_name
@env['SCRIPT_NAME'] || ''
end
def session
@env['rack.session']
end
def request
@request ||= Rack::Request.new(@env)
end
def name
options.name
end
def redirect(uri)
r = Rack::Response.new
if options[:iframe]
r.write("<script type='text/javascript' charset='utf-8'>top.location.href = '#{uri}';</script>")
else
r.write("Redirecting to #{uri}...")
r.redirect(uri)
end
r.finish
end
def user_info; {} end
def fail!(message_key, exception = nil)
self.env['omniauth.error'] = exception
self.env['omniauth.error.type'] = message_key.to_sym
self.env['omniauth.error.strategy'] = self
OmniAuth.config.on_failure.call(self.env)
end
class Options < Hashie::Mash; end
end
end