omniauth 2.1.4 → 2.3.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.
@@ -0,0 +1,118 @@
1
+ module OmniAuth
2
+ module Strategy
3
+ module ClassMethods
4
+ DISPATCH_METHODS = %i[call! phase_call options_call? request_call? on_auth_path?].freeze
5
+ private_constant :DISPATCH_METHODS
6
+
7
+ # True when requests are dispatched by OmniAuth's own #call! and its
8
+ # phase checks, so #call can tell which ones would go to the app.
9
+ # They may live in Strategy or in a module it includes, like Paths.
10
+ # Methods patched directly onto Strategy count as its own.
11
+ #
12
+ # @api private
13
+ def default_dispatch?
14
+ return @default_dispatch if instance_variable_defined?(:@default_dispatch)
15
+
16
+ @default_dispatch = DISPATCH_METHODS.all? { |m| Strategy.ancestors.include?(instance_method(m).owner) }
17
+ end
18
+
19
+ # Returns an inherited set of default options set at the class-level
20
+ # for each strategy.
21
+ def default_options
22
+ @default_options ||= begin
23
+ parent = superclass
24
+ Options.new((parent.default_options if parent.is_a?(ClassMethods)))
25
+ end
26
+ end
27
+
28
+ # This allows for more declarative subclassing of strategies by allowing
29
+ # default options to be set using a simple configure call.
30
+ #
31
+ # @param options [Hash] If supplied, these will be the default options (deep-merged into the superclass's default options).
32
+ # @yield [Options] The options Mash that allows you to set your defaults as you'd like.
33
+ #
34
+ # @example Using a yield to configure the default options.
35
+ #
36
+ # class MyStrategy
37
+ # include OmniAuth::Strategy
38
+ #
39
+ # configure do |c|
40
+ # c.foo = 'bar'
41
+ # end
42
+ # end
43
+ #
44
+ # @example Using a hash to configure the default options.
45
+ #
46
+ # class MyStrategy
47
+ # include OmniAuth::Strategy
48
+ # configure foo: 'bar'
49
+ # end
50
+ def configure(options = nil)
51
+ if block_given?
52
+ yield default_options
53
+ else
54
+ default_options.deep_merge!(options || {})
55
+ end
56
+ end
57
+
58
+ # Directly declare a default option for your class. This is a useful from
59
+ # a documentation perspective as it provides a simple line-by-line analysis
60
+ # of the kinds of options your strategy provides by default.
61
+ #
62
+ # @param name [Symbol] The key of the default option in your configuration hash.
63
+ # @param value [Object] The value your object defaults to. Nil if not provided.
64
+ #
65
+ # @example
66
+ #
67
+ # class MyStrategy
68
+ # include OmniAuth::Strategy
69
+ #
70
+ # option :foo, 'bar'
71
+ # option
72
+ # end
73
+ def option(name, value = nil)
74
+ default_options[name] = value
75
+ end
76
+
77
+ # Sets (and retrieves) option key names for initializer arguments to be
78
+ # recorded as. This takes care of 90% of the use cases for overriding
79
+ # the initializer in OmniAuth Strategies.
80
+ def args(args = nil)
81
+ if args
82
+ @args = args.is_a?(Array) ? args : [args]
83
+ return
84
+ end
85
+ return @args if instance_variable_defined?(:@args)
86
+
87
+ parent = superclass
88
+ parent.is_a?(ClassMethods) ? parent.args : []
89
+ end
90
+
91
+ %w[uid info extra credentials].each do |fetcher|
92
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
93
+ attr_reader :#{fetcher}_proc # attr_reader :uid_proc
94
+ private :#{fetcher}_proc # private :uid_proc
95
+
96
+ def #{fetcher}(&block) # def uid(&block)
97
+ return #{fetcher}_proc unless block_given? # return uid_proc unless block_given?
98
+ @#{fetcher}_proc = block # @uid_proc = block
99
+ end # end
100
+
101
+ def #{fetcher}_stack(context) # def uid_stack(context)
102
+ compile_stack(self.ancestors, :#{fetcher}, context) # compile_stack(self.ancestors, :uid, context)
103
+ end # end
104
+ RUBY
105
+ end
106
+
107
+ def compile_stack(ancestors, method, context)
108
+ stack = ancestors.inject([]) do |a, ancestor|
109
+ # Only strategy classes define these blocks. Other ancestors may
110
+ # respond to the same names (e.g. a monkeypatched Object#info).
111
+ a << context.instance_eval(&ancestor.public_send(method)) if ancestor.is_a?(ClassMethods) && ancestor.public_send(method)
112
+ a
113
+ end
114
+ stack.reverse
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,57 @@
1
+ module OmniAuth
2
+ module Strategy
3
+ # Builds the auth hash from the uid, info, credentials, and extra
4
+ # blocks a strategy declares with the class-level fetcher DSL.
5
+ module Fetchers
6
+ def uid
7
+ self.class.uid_stack(self).last
8
+ end
9
+
10
+ def info
11
+ merge_stack(self.class.info_stack(self))
12
+ end
13
+
14
+ def credentials
15
+ merge_stack(self.class.credentials_stack(self))
16
+ end
17
+
18
+ def extra
19
+ merge_stack(self.class.extra_stack(self))
20
+ end
21
+
22
+ def auth_hash
23
+ credentials_data = credentials
24
+ extra_data = extra
25
+ AuthHash.new(:provider => name, :uid => uid).tap do |auth|
26
+ auth.info = info unless skip_info?
27
+ auth.credentials = credentials_data if credentials_data
28
+ auth.extra = extra_data if extra_data
29
+ end
30
+ end
31
+
32
+ # Determines whether or not user info should be retrieved. This
33
+ # allows some strategies to save a call to an external API service
34
+ # for existing users. You can use it either by setting the `:skip_info`
35
+ # to true or by setting `:skip_info` to a Proc that takes a uid and
36
+ # evaluates to true when you would like to skip info.
37
+ #
38
+ # @example
39
+ #
40
+ # use MyStrategy, :skip_info => lambda{|uid| User.find_by_uid(uid)}
41
+ def skip_info?
42
+ return false unless options.skip_info?
43
+ return true unless options.skip_info.respond_to?(:call)
44
+
45
+ options.skip_info.call(uid)
46
+ end
47
+
48
+ protected
49
+
50
+ def merge_stack(stack)
51
+ merged = {} #: Hash[untyped, untyped]
52
+ stack.each { |hash| merged.merge!(hash) }
53
+ merged
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,48 @@
1
+ module OmniAuth
2
+ module Strategy
3
+ # Guards against forged hosts. Unless a full_host is set, callback URLs
4
+ # use the request's host, which comes from the Host or X-Forwarded-Host
5
+ # header, so a forged one would send the provider a redirect_uri on
6
+ # another host.
7
+ module Hosts
8
+ protected
9
+
10
+ # Whether callback URLs may use this request's host. They always may
11
+ # when a full_host is set, since then the request's host isn't used.
12
+ # Otherwise, if OmniAuth.config.allowed_hosts is set, the host must
13
+ # match one of its entries: a string matches the whole host, ignoring
14
+ # case, and a regexp must match the whole host.
15
+ def allowed_host?
16
+ return true if full_host_set?
17
+
18
+ allowed_hosts = OmniAuth.config.allowed_hosts
19
+ return true unless allowed_hosts
20
+
21
+ host = request.host
22
+ allowed_hosts.any? do |allowed|
23
+ allowed.instance_of?(Regexp) ? /\A(?:#{allowed})\z/ === host : allowed.casecmp?(host)
24
+ end
25
+ end
26
+
27
+ def full_host_set?
28
+ !(options[:full_host] || OmniAuth.config.full_host).nil?
29
+ end
30
+
31
+ def warn_if_using_request_host
32
+ return if full_host_set? || OmniAuth.config.allowed_hosts
33
+
34
+ log :warn, <<-WARN
35
+ OmniAuth is building callback URLs from the request's host, which comes
36
+ from the Host or X-Forwarded-Host header and can be forged unless your
37
+ app or a proxy in front of it checks it. To fix this, set either:
38
+ OmniAuth.config.full_host = 'https://example.com'
39
+ OmniAuth.config.allowed_hosts = ['example.com']
40
+ WARN
41
+ end
42
+
43
+ def invalid_host!
44
+ fail!(:invalid_host, Error.new('The request host is not in OmniAuth.config.allowed_hosts'))
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,140 @@
1
+ module OmniAuth
2
+ module Strategy
3
+ # Works out the request, callback, and setup paths for a strategy and
4
+ # whether the current request is on one of them, plus the full URLs
5
+ # built from them.
6
+ module Paths
7
+ # Returns true if the environment recognizes either the
8
+ # request or callback path.
9
+ def on_auth_path?
10
+ on_request_path? || on_callback_path?
11
+ end
12
+
13
+ def on_request_path?
14
+ if options[:request_path].respond_to?(:call)
15
+ options.fetch(:request_path).call(env)
16
+ else
17
+ on_path?(request_path)
18
+ end
19
+ end
20
+
21
+ def on_callback_path?
22
+ on_path?(callback_path) || current_path.sub(FORMAT_EXTENSION_REGEX, EMPTY_STRING).casecmp(callback_path).zero?
23
+ end
24
+
25
+ def on_path?(path)
26
+ current_path.casecmp(path).zero?
27
+ end
28
+
29
+ def path_prefix
30
+ options[:path_prefix] || OmniAuth.config.path_prefix
31
+ end
32
+
33
+ def custom_path(kind)
34
+ if options[kind].respond_to?(:call)
35
+ result = options.fetch(kind).call(env)
36
+ return nil unless result.is_a?(String)
37
+
38
+ result
39
+ else
40
+ options[kind]
41
+ end
42
+ end
43
+
44
+ def request_path
45
+ @request_path ||=
46
+ if options[:request_path].is_a?(String)
47
+ options.fetch(:request_path)
48
+ else
49
+ "#{script_name}#{path_prefix}/#{name}"
50
+ end
51
+ end
52
+
53
+ def callback_path
54
+ @callback_path ||= callback_path_option || custom_path(:request_path) || "#{script_name}#{path_prefix}/#{name}/callback"
55
+ end
56
+
57
+ def setup_path
58
+ options[:setup_path] || "#{path_prefix}/#{name}/setup"
59
+ end
60
+
61
+ CURRENT_PATH_REGEX = %r{/$}.freeze
62
+ FORMAT_EXTENSION_REGEX = /\.\w+\z/.freeze
63
+ EMPTY_STRING = ''.freeze
64
+ # Repeated slashes are collapsed, as Rails routing does, so a proxy
65
+ # that sends //auth/:provider still reaches the strategy.
66
+ def current_path
67
+ @current_path ||= request.path.downcase.squeeze('/').sub(CURRENT_PATH_REGEX, EMPTY_STRING)
68
+ end
69
+
70
+ def query_string
71
+ request.query_string.empty? ? '' : "?#{request.query_string}"
72
+ end
73
+
74
+ # The scheme and host for URLs such as callback_url: the strategy's
75
+ # :full_host option, else OmniAuth.config.full_host, else the
76
+ # request's. Either setting can be a string or a proc taking the env.
77
+ def full_host
78
+ host = options[:full_host] || OmniAuth.config.full_host
79
+ case host
80
+ when String then host
81
+ when Proc then host.call(env)
82
+ else full_host_from_request
83
+ end
84
+ end
85
+
86
+ def callback_url
87
+ full_host + callback_path + query_string
88
+ end
89
+
90
+ def script_name
91
+ current_env = @env
92
+ return '' if current_env.nil?
93
+
94
+ current_env['SCRIPT_NAME'] || ''
95
+ end
96
+
97
+ protected
98
+
99
+ # The :callback_path option: a string, or a lambda that returns true
100
+ # when the current path is the callback path.
101
+ def callback_path_option
102
+ option = options[:callback_path]
103
+ return option if option.is_a?(String)
104
+
105
+ current_path if option.respond_to?(:call) && option.call(env)
106
+ end
107
+
108
+ # The scheme, host, and port of the current request, or an empty
109
+ # string if the request URL isn't absolute.
110
+ def full_host_from_request
111
+ # in Rack 1.3.x, request.url explodes if scheme is nil
112
+ return '' unless request.scheme && URI.parse(request.url).absolute?
113
+
114
+ uri = URI.parse(request.url.sub(/\?.*/, ''))
115
+ uri.path = ''
116
+ # sometimes the url is actually showing http inside rails because the
117
+ # other layers (like nginx) have handled the ssl termination.
118
+ uri.scheme = 'https' if ssl?
119
+ String(uri)
120
+ end
121
+
122
+ # Rack env values that mean the request came in over HTTPS.
123
+ SSL_ENV_VALUES = {
124
+ 'HTTPS' => 'on',
125
+ 'HTTP_X_FORWARDED_SSL' => 'on',
126
+ 'HTTP_X_FORWARDED_SCHEME' => 'https',
127
+ 'rack.url_scheme' => 'https'
128
+ }.freeze
129
+
130
+ # Reads request.env rather than env, so full_host and callback_url also
131
+ # work outside a Rack call, as when a strategy's tests stub #request.
132
+ def ssl?
133
+ request_env = request.env
134
+ # A proxy chain can list several protocols; the first is the client's.
135
+ forwarded_proto = request_env['HTTP_X_FORWARDED_PROTO'].to_s.split(',').first
136
+ forwarded_proto.eql?('https') || SSL_ENV_VALUES.any? { |key, value| request_env[key].eql?(value) }
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,57 @@
1
+ module OmniAuth
2
+ module Strategy
3
+ # Keeps the origin and params of several sign-ins in progress at once,
4
+ # for example in several tabs, so each callback gets its own. Without
5
+ # this, the session holds a single omniauth.origin and omniauth.params,
6
+ # and starting a sign-in replaces the last one's.
7
+ #
8
+ # A strategy opts in when its provider sends a value from the request
9
+ # phase back to the callback, such as OAuth 2.0's state. Its request
10
+ # phase calls remember_sign_in with that value, and it overrides
11
+ # callback_sign_in_key to return the value from the callback.
12
+ module SignIns
13
+ # How many sign-ins are remembered. Remembering another forgets the
14
+ # oldest.
15
+ MAX_REMEMBERED_SIGN_INS = 5
16
+
17
+ # Remembers the origin and params stored for the current request phase
18
+ # under key, which the provider sends back to the callback.
19
+ def remember_sign_in(key)
20
+ return if key.empty?
21
+
22
+ sign_ins = remembered_sign_ins
23
+ sign_ins.delete(key)
24
+ sign_ins[key] = {'origin' => session['omniauth.origin'], 'params' => session.fetch('omniauth.params')}
25
+ session['omniauth.sign_ins'] = sign_ins.to_a.last(MAX_REMEMBERED_SIGN_INS).to_h
26
+ end
27
+
28
+ # The key the callback sends back, which a strategy that calls
29
+ # remember_sign_in returns. Nil for a strategy that doesn't.
30
+ def callback_sign_in_key; end
31
+
32
+ protected
33
+
34
+ # The origin and params for this callback, removed from the session:
35
+ # the ones remembered under its key, or else omniauth.origin and
36
+ # omniauth.params, which hold the latest sign-in's.
37
+ def recall_sign_in
38
+ fallback = {'origin' => session.delete('omniauth.origin'), 'params' => session.delete('omniauth.params')}
39
+ key = callback_sign_in_key
40
+ sign_ins = remembered_sign_ins
41
+ remembered = sign_ins.keys.detect { |stored| Rack::Utils.secure_compare(stored, key) } if key.instance_of?(String)
42
+ sign_in = sign_ins.delete(remembered)
43
+ return fallback unless sign_in
44
+
45
+ session['omniauth.sign_ins'] = sign_ins
46
+ sign_in
47
+ end
48
+
49
+ # A copy of the remembered sign-ins, which are changed by assigning the
50
+ # session key, not in place.
51
+ def remembered_sign_ins
52
+ sign_ins = session['omniauth.sign_ins']
53
+ sign_ins.is_a?(Hash) ? sign_ins.dup : {}
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,53 @@
1
+ module OmniAuth
2
+ module Strategy
3
+ # Runs the request and callback phases against OmniAuth.config.mock_auth
4
+ # instead of the provider when OmniAuth.config.test_mode is enabled.
5
+ module TestMode
6
+ # This is called in lieu of the normal request process
7
+ # in the event that OmniAuth has been configured to be
8
+ # in test mode.
9
+ def mock_call!(*)
10
+ if request_call?
11
+ mock_request_call
12
+ elsif on_callback_path?
13
+ mock_callback_call
14
+ else
15
+ call_app!
16
+ end
17
+ rescue StandardError => e
18
+ handle_phase_error(e)
19
+ end
20
+
21
+ def mock_request_call
22
+ setup_phase
23
+
24
+ run_hook(:request_validation_phase)
25
+
26
+ session['omniauth.params'] = omniauth_params
27
+
28
+ run_hook(:before_request_phase)
29
+
30
+ store_origin if options.origin_param
31
+
32
+ result = redirect(callback_url)
33
+
34
+ run_hook(:after_request_phase)
35
+ result
36
+ end
37
+
38
+ def mock_callback_call
39
+ setup_phase
40
+
41
+ origin = session.delete('omniauth.origin')
42
+ env['omniauth.origin'] ||= origin
43
+ env['omniauth.origin'] = nil if env.fetch('omniauth.origin').eql?('')
44
+ env['omniauth.params'] = session.delete('omniauth.params') || {}
45
+
46
+ mocked_auth = OmniAuth.mock_auth_for(name)
47
+ env['omniauth.auth'] = mocked_auth unless mocked_auth.instance_of?(Symbol)
48
+ run_hook(:before_callback_phase)
49
+ mocked_auth.instance_of?(Symbol) ? fail!(mocked_auth) : call_app!
50
+ end
51
+ end
52
+ end
53
+ end