omniauth 0.3.0.rc3 → 1.0.0.pr1

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.
@@ -0,0 +1,191 @@
1
+ require 'omniauth'
2
+
3
+ module OmniAuth
4
+ class Form
5
+ DEFAULT_CSS = <<-CSS
6
+ body {
7
+ background: #ccc;
8
+ font-family: "Lucida Grande", "Lucida Sans", Helvetica, Arial, sans-serif;
9
+ }
10
+
11
+ h1 {
12
+ text-align: center;
13
+ margin: 30px auto 0px;
14
+ font-size: 18px;
15
+ padding: 10px 10px 15px;
16
+ background: #555;
17
+ color: white;
18
+ width: 320px;
19
+ border: 10px solid #444;
20
+ border-bottom: 0;
21
+ -moz-border-radius-topleft: 10px;
22
+ -moz-border-radius-topright: 10px;
23
+ -webkit-border-top-left-radius: 10px;
24
+ -webkit-border-top-right-radius: 10px;
25
+ border-top-left-radius: 10px;
26
+ border-top-right-radius: 10px;
27
+ }
28
+
29
+ h1, form {
30
+ -moz-box-shadow: 2px 2px 7px rgba(0,0,0,0.3);
31
+ -webkit-box-shadow: 2px 2px 7px rgba(0,0,0,0.3);
32
+ }
33
+
34
+ form {
35
+ background: white;
36
+ border: 10px solid #eee;
37
+ border-top: 0;
38
+ padding: 20px;
39
+ margin: 0px auto 40px;
40
+ width: 300px;
41
+ -moz-border-radius-bottomleft: 10px;
42
+ -moz-border-radius-bottomright: 10px;
43
+ -webkit-border-bottom-left-radius: 10px;
44
+ -webkit-border-bottom-right-radius: 10px;
45
+ border-bottom-left-radius: 10px;
46
+ border-bottom-right-radius: 10px;
47
+ }
48
+
49
+ label {
50
+ display: block;
51
+ font-weight: bold;
52
+ margin-bottom: 5px;
53
+ }
54
+
55
+ input {
56
+ font-size: 18px;
57
+ padding: 4px 8px;
58
+ display: block;
59
+ margin-bottom: 10px;
60
+ width: 280px;
61
+ }
62
+
63
+ input#identifier, input#openid_url {
64
+ background: url(http://openid.net/login-bg.gif) no-repeat;
65
+ background-position: 0 50%;
66
+ padding-left: 18px;
67
+ }
68
+
69
+ button {
70
+ font-size: 22px;
71
+ padding: 4px 8px;
72
+ display: block;
73
+ margin: 20px auto 0;
74
+ }
75
+
76
+ fieldset {
77
+ border: 1px solid #ccc;
78
+ border-left: 0;
79
+ border-right: 0;
80
+ padding: 10px 0;
81
+ }
82
+
83
+ fieldset input {
84
+ width: 260px;
85
+ font-size: 16px;
86
+ }
87
+ CSS
88
+
89
+ attr_accessor :options
90
+
91
+ def initialize(options = {})
92
+ options[:title] ||= "Authentication Info Required"
93
+ options[:header_info] ||= ""
94
+ self.options = options
95
+
96
+ @html = ""
97
+ header(options[:title],options[:header_info])
98
+ end
99
+
100
+ def self.build(title=nil,&block)
101
+ form = OmniAuth::Form.new(:title => title)
102
+ if block.arity > 0
103
+ yield form
104
+ else
105
+ form.instance_eval(&block)
106
+ end
107
+ form
108
+ end
109
+
110
+ def label_field(text, target)
111
+ @html << "\n<label for='#{target}'>#{text}:</label>"
112
+ self
113
+ end
114
+
115
+ def input_field(type, name)
116
+ @html << "\n<input type='#{type}' id='#{name}' name='#{name}'/>"
117
+ self
118
+ end
119
+
120
+ def text_field(label, name)
121
+ label_field(label, name)
122
+ input_field('text', name)
123
+ self
124
+ end
125
+
126
+ def password_field(label, name)
127
+ label_field(label, name)
128
+ input_field('password', name)
129
+ self
130
+ end
131
+
132
+ def button(text)
133
+ @html << "\n<button type='submit'>#{text}</button>"
134
+ end
135
+
136
+ def html(html)
137
+ @html << html
138
+ end
139
+
140
+ def fieldset(legend, options = {}, &block)
141
+ @html << "\n<fieldset#{" style='#{options[:style]}'" if options[:style]}#{" id='#{options[:id]}'" if options[:id]}>\n <legend>#{legend}</legend>\n"
142
+ self.instance_eval &block
143
+ @html << "\n</fieldset>"
144
+ self
145
+ end
146
+
147
+ def header(title,header_info)
148
+ @html << <<-HTML
149
+ <!DOCTYPE html>
150
+ <html>
151
+ <head>
152
+ <title>#{title}</title>
153
+ #{css}
154
+ #{header_info}
155
+ </head>
156
+ <body>
157
+ <h1>#{title}</h1>
158
+ <form method='post' #{"action='#{options[:url]}' " if options[:url]}noValidate='noValidate'>
159
+ HTML
160
+ self
161
+ end
162
+
163
+ def footer
164
+ return self if @footer
165
+ @html << <<-HTML
166
+ <button type='submit'>Connect</button>
167
+ </form>
168
+ </body>
169
+ </html>
170
+ HTML
171
+ @footer = true
172
+ self
173
+ end
174
+
175
+ def to_html
176
+ footer
177
+ @html
178
+ end
179
+
180
+ def to_response
181
+ footer
182
+ Rack::Response.new(@html).finish
183
+ end
184
+
185
+ protected
186
+
187
+ def css
188
+ "\n<style type='text/css'>#{OmniAuth.config.form_css}</style>"
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,429 @@
1
+ require 'omniauth'
2
+ require 'hashie/mash'
3
+
4
+ module OmniAuth
5
+ class NoSessionError < StandardError; end
6
+ # The Strategy is the base unit of OmniAuth's ability to
7
+ # wrangle multiple providers. Each strategy provided by
8
+ # OmniAuth includes this mixin to gain the default functionality
9
+ # necessary to be compatible with the OmniAuth library.
10
+ module Strategy
11
+ def self.included(base)
12
+ OmniAuth.strategies << base
13
+
14
+ base.extend ClassMethods
15
+ base.class_eval do
16
+ attr_reader :app, :env, :options, :response
17
+
18
+ option :setup, false
19
+ option :skip_info, false
20
+ end
21
+ end
22
+
23
+ module ClassMethods
24
+ # Returns an inherited set of default options set at the class-level
25
+ # for each strategy.
26
+ def default_options
27
+ return @default_options if @default_options
28
+ existing = superclass.respond_to?(:default_options) ? superclass.default_options : {}
29
+ @default_options = OmniAuth::Strategy::Options.new(existing)
30
+ end
31
+
32
+ # This allows for more declarative subclassing of strategies by allowing
33
+ # default options to be set using a simple configure call.
34
+ #
35
+ # @param options [Hash] If supplied, these will be the default options (deep-merged into the superclass's default options).
36
+ # @yield [Options] The options Mash that allows you to set your defaults as you'd like.
37
+ #
38
+ # @example Using a yield to configure the default options.
39
+ #
40
+ # class MyStrategy
41
+ # include OmniAuth::Strategy
42
+ #
43
+ # configure do |c|
44
+ # c.foo = 'bar'
45
+ # end
46
+ # end
47
+ #
48
+ # @example Using a hash to configure the default options.
49
+ #
50
+ # class MyStrategy
51
+ # include OmniAuth::Strategy
52
+ # configure foo: 'bar'
53
+ # end
54
+ def configure(options = nil)
55
+ yield default_options and return unless options
56
+ default_options.deep_merge!(options)
57
+ end
58
+
59
+ # Directly declare a default option for your class. This is a useful from
60
+ # a documentation perspective as it provides a simple line-by-line analysis
61
+ # of the kinds of options your strategy provides by default.
62
+ #
63
+ # @param name [Symbol] The key of the default option in your configuration hash.
64
+ # @param value [Object] The value your object defaults to. Nil if not provided.
65
+ #
66
+ # @example
67
+ #
68
+ # class MyStrategy
69
+ # include OmniAuth::Strategy
70
+ #
71
+ # option :foo, 'bar'
72
+ # option
73
+ # end
74
+ def option(name, value = nil)
75
+ default_options[name] = value
76
+ end
77
+
78
+ # Sets (and retrieves) option key names for initializer arguments to be
79
+ # recorded as. This takes care of 90% of the use cases for overriding
80
+ # the initializer in OmniAuth Strategies.
81
+ def args(args = nil)
82
+ @args = Array(args) and return if args
83
+ existing = superclass.respond_to?(:args) ? superclass.args : []
84
+ return @args || existing
85
+ end
86
+
87
+ %w(uid info extra credentials).each do |fetcher|
88
+ class_eval <<-RUBY
89
+ def #{fetcher}(&block)
90
+ return @#{fetcher}_proc unless block_given?
91
+ @#{fetcher}_proc = block
92
+ end
93
+
94
+ def #{fetcher}_stack(context)
95
+ compile_stack(self.ancestors, :#{fetcher}, context)
96
+ end
97
+ RUBY
98
+ end
99
+
100
+ def compile_stack(ancestors, method, context)
101
+ stack = ancestors.inject([]) do |a, ancestor|
102
+ a << context.instance_eval(&ancestor.send(method)) if ancestor.respond_to?(method) && ancestor.send(method)
103
+ a
104
+ end
105
+ stack.reverse!
106
+ end
107
+ end
108
+
109
+ # Initializes the strategy by passing in the Rack endpoint,
110
+ # the unique URL segment name for this strategy, and any
111
+ # additional arguments. An `options` hash is automatically
112
+ # created from the last argument if it is a hash.
113
+ #
114
+ # @param app [Rack application] The application on which this middleware is applied.
115
+ #
116
+ # @overload new(app, options = {})
117
+ # If nothing but a hash is supplied, initialized with the supplied options
118
+ # overriding the strategy's default options via a deep merge.
119
+ # @overload new(app, *args, options = {})
120
+ # If the strategy has supplied custom arguments that it accepts, they may
121
+ # will be passed through and set to the appropriate values.
122
+ #
123
+ # @yield [Options] Yields options to block for further configuration.
124
+ def initialize(app, *args, &block)
125
+ @app = app
126
+ @options = self.class.default_options.dup
127
+
128
+ options.deep_merge!(args.pop) if args.last.is_a?(Hash)
129
+ options.name ||= self.class.to_s.split('::').last.downcase
130
+
131
+ self.class.args.each do |arg|
132
+ options[arg] = args.shift
133
+ end
134
+
135
+ # Make sure that all of the args have been dealt with, otherwise error out.
136
+ raise ArgumentError, "Received wrong number of arguments. #{args.inspect}" unless args.empty?
137
+
138
+ yield options if block_given?
139
+ end
140
+
141
+ def inspect
142
+ "#<#{self.class.to_s}>"
143
+ end
144
+
145
+ # Duplicates this instance and runs #call! on it.
146
+ # @param [Hash] The Rack environment.
147
+ def call(env)
148
+ dup.call!(env)
149
+ end
150
+
151
+ # The logic for dispatching any additional actions that need
152
+ # to be taken. For instance, calling the request phase if
153
+ # the request path is recognized.
154
+ #
155
+ # @param env [Hash] The Rack environment.
156
+ def call!(env)
157
+ raise OmniAuth::NoSessionError.new("You must provide a session to use OmniAuth.") unless env['rack.session']
158
+
159
+ @env = env
160
+ @env['omniauth.strategy'] = self if on_auth_path?
161
+
162
+ return mock_call!(env) if OmniAuth.config.test_mode
163
+
164
+ return options_call if on_auth_path? && options_request?
165
+ return request_call if on_request_path? && OmniAuth.config.allowed_request_methods.include?(request.request_method.downcase.to_sym)
166
+ return callback_call if on_callback_path?
167
+ return other_phase if respond_to?(:other_phase)
168
+ @app.call(env)
169
+ end
170
+
171
+ # Responds to an OPTIONS request.
172
+ def options_call
173
+ verbs = OmniAuth.config.allowed_request_methods.map(&:to_s).map(&:upcase).join(', ')
174
+ return [ 200, { 'Allow' => verbs }, [] ]
175
+ end
176
+
177
+ # Performs the steps necessary to run the request phase of a strategy.
178
+ def request_call
179
+ setup_phase
180
+ if options.form.respond_to?(:call)
181
+ options.form.call(env)
182
+ elsif options.form
183
+ call_app!
184
+ else
185
+ if request.params['origin']
186
+ env['rack.session']['omniauth.origin'] = request.params['origin']
187
+ elsif env['HTTP_REFERER'] && !env['HTTP_REFERER'].match(/#{request_path}$/)
188
+ env['rack.session']['omniauth.origin'] = env['HTTP_REFERER']
189
+ end
190
+ request_phase
191
+ end
192
+ end
193
+
194
+ # Performs the steps necessary to run the callback phase of a strategy.
195
+ def callback_call
196
+ setup_phase
197
+ @env['omniauth.origin'] = session.delete('omniauth.origin')
198
+ @env['omniauth.origin'] = nil if env['omniauth.origin'] == ''
199
+ @env['omniauth.params'] = session.delete('query_params') || {}
200
+ callback_phase
201
+ end
202
+
203
+ # Returns true if the environment recognizes either the
204
+ # request or callback path.
205
+ def on_auth_path?
206
+ on_request_path? || on_callback_path?
207
+ end
208
+
209
+ def on_request_path?
210
+ on_path?(request_path)
211
+ end
212
+
213
+ def on_callback_path?
214
+ on_path?(callback_path)
215
+ end
216
+
217
+ def on_path?(path)
218
+ current_path.casecmp(path) == 0
219
+ end
220
+
221
+ def options_request?
222
+ request.request_method == 'OPTIONS'
223
+ end
224
+
225
+ # This is called in lieu of the normal request process
226
+ # in the event that OmniAuth has been configured to be
227
+ # in test mode.
228
+ def mock_call!(env)
229
+ return mock_request_call if on_request_path?
230
+ return mock_callback_call if on_callback_path?
231
+ call_app!
232
+ end
233
+
234
+ def mock_request_call
235
+ setup_phase
236
+ return response if response = call_through_to_app
237
+
238
+ if request.params['origin']
239
+ @env['rack.session']['omniauth.origin'] = request.params['origin']
240
+ elsif env['HTTP_REFERER'] && !env['HTTP_REFERER'].match(/#{request_path}$/)
241
+ @env['rack.session']['omniauth.origin'] = env['HTTP_REFERER']
242
+ end
243
+ redirect(script_name + callback_path + query_string)
244
+ end
245
+
246
+ def mock_callback_call
247
+ setup_phase
248
+ mocked_auth = OmniAuth.mock_auth_for(name.to_s)
249
+ if mocked_auth.is_a?(Symbol)
250
+ fail!(mocked_auth)
251
+ else
252
+ @env['omniauth.auth'] = mocked_auth
253
+ @env['omniauth.origin'] = session.delete('omniauth.origin')
254
+ @env['omniauth.origin'] = nil if env['omniauth.origin'] == ''
255
+ call_app!
256
+ end
257
+ end
258
+
259
+ # The setup phase looks for the `:setup` option to exist and,
260
+ # if it is, will call either the Rack endpoint supplied to the
261
+ # `:setup` option or it will call out to the setup path of the
262
+ # underlying application. This will default to `/auth/:provider/setup`.
263
+ def setup_phase
264
+ if options[:setup].respond_to?(:call)
265
+ options[:setup].call(env)
266
+ elsif options.setup?
267
+ setup_env = env.merge('PATH_INFO' => setup_path, 'REQUEST_METHOD' => 'GET')
268
+ call_app!(setup_env)
269
+ end
270
+ end
271
+
272
+ # @abstract This method is called when the user is on the request path. You should
273
+ # perform any information gathering you need to be able to authenticate
274
+ # the user in this phase.
275
+ def request_phase
276
+ raise NotImplementedError
277
+ end
278
+
279
+ def uid
280
+ self.class.uid_stack(self).last
281
+ end
282
+
283
+ def info
284
+ merge_stack(self.class.info_stack(self))
285
+ end
286
+
287
+ def credentials
288
+ merge_stack(self.class.credentials_stack(self))
289
+ end
290
+
291
+ def extra
292
+ merge_stack(self.class.extra_stack(self))
293
+ end
294
+
295
+ def auth_hash
296
+ hash = AuthHash.new(:provider => name, :uid => uid)
297
+ hash.info = info unless skip_info?
298
+ hash.credentials = credentials if credentials
299
+ hash.extra = extra if extra
300
+ hash
301
+ end
302
+
303
+ # Determines whether or not user info should be retrieved. This
304
+ # allows some strategies to save a call to an external API service
305
+ # for existing users. You can use it either by setting the `:skip_info`
306
+ # to true or by setting `:skip_info` to a Proc that takes a uid and
307
+ # evaluates to true when you would like to skip info.
308
+ #
309
+ # @example
310
+ #
311
+ # use MyStrategy, :skip_info => lambda{|uid| User.find_by_uid(uid)}
312
+ def skip_info?
313
+ if options.skip_info?
314
+ if options.skip_info.respond_to?(:call)
315
+ return options.skip_info.call(uid)
316
+ else
317
+ return true
318
+ end
319
+ end
320
+ false
321
+ end
322
+
323
+ def callback_phase
324
+ self.env['omniauth.auth'] = auth_hash
325
+ call_app!
326
+ end
327
+
328
+ def path_prefix
329
+ options[:path_prefix] || OmniAuth.config.path_prefix
330
+ end
331
+
332
+ def request_path
333
+ options[:request_path] || "#{path_prefix}/#{name}"
334
+ end
335
+
336
+ def callback_path
337
+ options[:callback_path] || "#{path_prefix}/#{name}/callback"
338
+ end
339
+
340
+ def setup_path
341
+ options[:setup_path] || "#{path_prefix}/#{name}/setup"
342
+ end
343
+
344
+ def current_path
345
+ request.path_info.downcase.sub(/\/$/,'')
346
+ end
347
+
348
+ def query_string
349
+ request.query_string.empty? ? "" : "?#{request.query_string}"
350
+ end
351
+
352
+ def call_through_to_app
353
+ status, headers, body = *call_app!
354
+ session['query_params'] = Rack::Request.new(env).params
355
+ @response = Rack::Response.new(body, status, headers)
356
+
357
+ status == 404 ? nil : @response.finish
358
+ end
359
+
360
+ def call_app!(env = @env)
361
+ @app.call(env)
362
+ end
363
+
364
+ def full_host
365
+ case OmniAuth.config.full_host
366
+ when String
367
+ OmniAuth.config.full_host
368
+ when Proc
369
+ OmniAuth.config.full_host.call(env)
370
+ else
371
+ uri = URI.parse(request.url.gsub(/\?.*$/,''))
372
+ uri.path = ''
373
+ uri.query = nil
374
+ uri.to_s
375
+ end
376
+ end
377
+
378
+ def callback_url
379
+ full_host + script_name + callback_path + query_string
380
+ end
381
+
382
+ def script_name
383
+ @env['SCRIPT_NAME'] || ''
384
+ end
385
+
386
+ def session
387
+ @env['rack.session']
388
+ end
389
+
390
+ def request
391
+ @request ||= Rack::Request.new(@env)
392
+ end
393
+
394
+ def name
395
+ options.name
396
+ end
397
+
398
+ def redirect(uri)
399
+ r = Rack::Response.new
400
+
401
+ if options[:iframe]
402
+ r.write("<script type='text/javascript' charset='utf-8'>top.location.href = '#{uri}';</script>")
403
+ else
404
+ r.write("Redirecting to #{uri}...")
405
+ r.redirect(uri)
406
+ end
407
+
408
+ r.finish
409
+ end
410
+
411
+ def user_info; {} end
412
+
413
+ def fail!(message_key, exception = nil)
414
+ self.env['omniauth.error'] = exception
415
+ self.env['omniauth.error.type'] = message_key.to_sym
416
+ self.env['omniauth.error.strategy'] = self
417
+
418
+ OmniAuth.config.on_failure.call(self.env)
419
+ end
420
+
421
+ class Options < Hashie::Mash; end
422
+
423
+ protected
424
+
425
+ def merge_stack(stack)
426
+ stack.inject({}){|c,h| c.merge!(h); c}
427
+ end
428
+ end
429
+ end
@@ -0,0 +1,8 @@
1
+ class OmniAuth::Test::PhonySession
2
+ def initialize(app); @app = app end
3
+ def call(env)
4
+ @session ||= (env['rack.session'] || {})
5
+ env['rack.session'] = @session
6
+ @app.call(env)
7
+ end
8
+ end
@@ -0,0 +1,34 @@
1
+ module OmniAuth
2
+
3
+ module Test
4
+
5
+ module StrategyMacros
6
+
7
+ def sets_an_auth_hash
8
+ it 'should set an auth hash' do
9
+ last_request.env['omniauth.auth'].should be_kind_of(Hash)
10
+ end
11
+ end
12
+
13
+ def sets_provider_to(provider)
14
+ it "should set the provider to #{provider}" do
15
+ (last_request.env['omniauth.auth'] || {})['provider'].should == provider
16
+ end
17
+ end
18
+
19
+ def sets_uid_to(uid)
20
+ it "should set the UID to #{uid}" do
21
+ (last_request.env['omniauth.auth'] || {})['uid'].should == uid
22
+ end
23
+ end
24
+
25
+ def sets_user_info_to(user_info)
26
+ it "should set the user_info to #{user_info}" do
27
+ (last_request.env['omniauth.auth'] || {})['user_info'].should == user_info
28
+ end
29
+ end
30
+ end
31
+
32
+ end
33
+
34
+ end
@@ -0,0 +1,49 @@
1
+ require 'rack'
2
+ require 'omniauth/test'
3
+
4
+ module OmniAuth
5
+
6
+ module Test
7
+
8
+ # Support for testing OmniAuth strategies.
9
+ #
10
+ # @example Usage
11
+ # class MyStrategyTest < Test::Unit::TestCase
12
+ # include OmniAuth::Test::StrategyTestCase
13
+ # def strategy
14
+ # # return the parameters to a Rack::Builder map call:
15
+ # [MyStrategy.new, :some, :configuration, :options => 'here']
16
+ # end
17
+ # setup do
18
+ # post '/auth/my_strategy/callback', :user => { 'name' => 'Dylan', 'id' => '445' }
19
+ # end
20
+ # end
21
+ module StrategyTestCase
22
+
23
+ def app
24
+ strat = self.strategy
25
+ resp = self.app_response
26
+ Rack::Builder.new {
27
+ use OmniAuth::Test::PhonySession
28
+ use *strat
29
+ run lambda {|env| [404, {'Content-Type' => 'text/plain'}, [resp || env.key?('omniauth.auth').to_s]] }
30
+ }.to_app
31
+ end
32
+
33
+ def app_response
34
+ nil
35
+ end
36
+
37
+ def session
38
+ last_request.env['rack.session']
39
+ end
40
+
41
+ def strategy
42
+ raise NotImplementedError.new('Including specs must define #strategy')
43
+ end
44
+
45
+ end
46
+
47
+ end
48
+
49
+ end