signet-rails 0.0.8 → 0.0.9

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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a1ad0e819bbfeceb6ab6795c49ca30d37ee15986
4
+ data.tar.gz: 9713281a98476b5ed795506df87b93a0686881dd
5
+ SHA512:
6
+ metadata.gz: 9f5205dcfea2bdf5411d46f371672ce810e81b04406c3e24a5477016332260a782420080ba4443f70a0ef67d5c6192dcf145429c8302132b21e1bfb523eeebd9
7
+ data.tar.gz: 1f078768bc115957fa5410657cfe579e174ab5d96ce03e15f1bf3267ca8f69401fb8467b182f5b5ae14baec7df8f2862db403c90f349d218f24c18c4b07d9e38
data/.gitignore CHANGED
@@ -15,3 +15,5 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ tags
19
+ bin
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
@@ -0,0 +1,5 @@
1
+ LineLength:
2
+ Max: 120
3
+
4
+ Documentation:
5
+ Enabled: false
@@ -3,136 +3,195 @@ require 'active_support/core_ext/string'
3
3
 
4
4
  module Signet
5
5
  module Rails
6
+
7
+ # The overall structure:
8
+ #
9
+ # Builder - used to create OAuth2 providers. These providers are Rack middleware
10
+ # and instances of Handler. A Builder can create multiple providers. See initialize
11
+ # def below
12
+ #
13
+ # Handler - Rack middleware that represents and instance of an OAuth2 provider
14
+ #
15
+ # Factory - a dumb factory that takes an env and returns an instance of
16
+ # a named Signet::OAuth2::Client
6
17
  class Builder < ::Rack::Builder
7
- @@default_options = {}
8
18
 
9
- def self.set_default_options opts = {}
10
- # normalize to symbol hash
11
- n_opts = opts.inject({}) { |memo,(k,v)| memo[k.to_sym] = v; memo }
12
- @@default_options = n_opts
19
+ class << self
20
+ attr_accessor :default_options
13
21
  end
14
22
 
15
- def initialize(app, &block)
16
- super
23
+ # User can set default_options for all providers via this static accessor
24
+ #
25
+ # TODO not very pretty.... can we refactor?
26
+ Builder.default_options = {}
27
+
28
+ # Options accepted by this Builder
29
+ #
30
+ # TODO obviously we can find a better way of packaging defaults e.g. Google setup
31
+ OPTIONS = {
32
+ # What returned OAuth2 values are persisted?
33
+ #
34
+ # Google config is the default. See:
35
+ #
36
+ # https://developers.google.com/accounts/docs/OAuth2WebServer#handlingtheresponse
37
+ #
38
+ # specifically, the section that deals with the 'shape' of the response to a request
39
+ # for an access token
40
+ persist_attrs: [:refresh_token, :access_token, :expires_in],
41
+
42
+ # What is the name of this provider? It will be used in the auth and auth_callback urls:
43
+ #
44
+ # /signet/google/auth - to authorise a user
45
+ # /signet/google/auth_callback - to handle the response from the OAuth2 provider
46
+ name: :google,
47
+
48
+ # What type is this provider? Is it a login-based OAuth2 adapter? If so, the callback
49
+ # will be used to identify a user and create one if necessary, specifically the uid
50
+ # in the response will be used as a secondary key in the user table
51
+ #
52
+ # Options:
53
+ # :login - as described above
54
+ # :webserver - the expectation will be that
55
+ type: :webserver,
56
+
57
+ # TODO need to define this better
58
+ storage_attr: :signet,
59
+
60
+ # TODO: see https://developers.google.com/accounts/docs/OAuth2Login#authenticationuriparameters
61
+ approval_prompt: 'auto',
62
+ authorization_uri: 'https://accounts.google.com/o/oauth2/auth',
63
+ token_credential_uri: 'https://accounts.google.com/o/oauth2/token',
64
+
65
+ # whether we handle the persistence of the auth callback or simply pass-through
66
+ handle_auth_callback: true,
67
+
68
+ # These need to be set either as default_options or per provider
69
+ client_id: nil,
70
+ client_secret: nil,
71
+
72
+ # redirect_uri can be set otherwise it will default based on the Rack env
73
+ redirect_uri: nil,
74
+
75
+ # A method which takes a Rack env and a Signet::OAuth2::Client (minus
76
+ # credentials) and returns a persistence wrapped object TODO finish
77
+ extract_credentials_from_env: nil,
78
+
79
+ # TODO
80
+ extract_credentials_from_env_by_oauth_id: nil,
81
+
82
+ # What connection object to use (Signet::OAuth2::Client will default to a Faraday default if
83
+ # this is not set)
84
+ connection: nil,
85
+ }
86
+
87
+ def self.set_default_options(opts = {})
88
+ Builder.default_options = opts.symbolize_keys
17
89
  end
18
90
 
19
- def provider(opts = {}, &block)
20
- # normalize to symbol hash
21
- n_opts = opts.inject({}) { |memo,(k,v)| memo[k.to_sym] = v; memo }
22
-
23
- # use the default_options as a base... then merge these changes on top
24
- combined_options = @@default_options.merge(n_opts)
25
-
26
- # now set some defaults if they aren't already set
27
-
28
- combined_options[:persist_attrs] ||= [:refresh_token, :access_token, :expires_in, :issued_at]
29
- combined_options[:name] ||= :google
30
-
31
- # is this a login-based OAuth2 adapter? If so, the callback will be used to identify a
32
- # user and create one if necessary
33
- # Options: :login, :webserver
34
- combined_options[:type] ||= :webserver
35
-
36
- # name of hash-behaving attribute on our wrapper that contains credentials
37
- # keyed by :name
38
- # {
39
- # "google": {
40
- # "uid": "012345676789abcde",
41
- # "refresh_token": "my_first_refresh_token",
42
- # "access_token": "my_first_access_token",
43
- # "expires_in": 123
44
- # }
45
- # }
46
- combined_options[:storage_attr] ||= :signet
47
-
48
- # TODO: see https://developers.google.com/accounts/docs/OAuth2Login#authenticationuriparameters
49
- combined_options[:approval_prompt] ||= 'auto'
50
-
51
- # unless specified, we need to set this at request-time because we need the env to get server etc
52
- # combined_options[:redirect_uri] = ??? need env
53
-
54
- # TODO: better way of sourcing these defaults... from signet?
55
- combined_options[:authorization_uri] ||= 'https://accounts.google.com/o/oauth2/auth'
56
- combined_options[:token_credential_uri] ||= 'https://accounts.google.com/o/oauth2/token'
57
-
58
- # whether we handle the persistence of the auth callback or simply pass-through
59
- combined_options[:handle_auth_callback] ||= true
60
-
61
- # The following lambda will be used when creating a new client in a factory
62
- # to get the persistence object
63
- combined_options[:extract_from_env] ||= lambda do |env, client|
64
- oac = nil
65
- session = env['rack.session']
66
- if !!session && !!session[:user_id]
67
- begin
68
- u = User.find(session[:user_id])
69
- oac = u.o_auth2_credentials.where(name: combined_options[:name]).first
70
- rescue ActiveRecord::RecordNotFound => e
71
- end
72
- end
73
- oac
74
- end
75
-
76
- # The following lambda will be used when handling the callback from the oauth server
77
- # In this flow we might not yet have established a session... need to handle two
78
- # flows, one for login, one not
79
- # when on a login auth_callback, how do we get the persistence object from the JWT?
80
- combined_options[:extract_by_oauth_id] ||= lambda do |env, client, id|
81
- oac = nil
82
- begin
83
- u = nil
84
- if combined_options[:type] == :login
85
- u = User.first_or_initialize(uid: combined_options[:name].to_s + "_" + id)
86
- u.save
87
- else
88
- session = env['rack.session']
89
- if !!session && !!session[:user_id]
90
- begin
91
- u = User.find(session[:user_id])
92
- rescue ActiveRecord::RecordNotFound => e
93
- end
94
- else
95
- raise "Expected to be able to find user in session"
96
- end
97
- end
98
-
99
- oac = u.o_auth2_credentials.first_or_initialize(name: combined_options[:name])
100
-
101
- rescue ActiveRecord::RecordNotFound => e
102
- end
103
-
104
- oac
105
- end
106
-
107
- combined_options[:persistence_wrapper] ||= :active_record
108
-
109
- # define a lambda that returns a lambda that wraps our OAC lambda return object
110
- # in a persistance object
111
- persistence_wrapper = lambda do |meth|
112
- lambda do |env, client, *args|
113
- y = meth.call env, client, *args
114
- klass_str = combined_options[:persistence_wrapper].to_s
115
- require "signet/rails/wrappers/#{klass_str}"
116
- w = "Signet::Rails::Wrappers::#{klass_str.camelize}".constantize.new y, client
117
- end
118
- end
119
-
120
- combined_options[:extract_by_oauth_id] = persistence_wrapper.call combined_options[:extract_by_oauth_id]
121
- combined_options[:extract_from_env] = persistence_wrapper.call combined_options[:extract_from_env]
122
-
123
- # TODO: check here we have the basics?
124
-
125
- # TODO: better auth_options split?
126
- auth_option_keys = [:prompt, :redirect_uri, :access_type, :approval_prompt, :client_id]
127
- base_options = combined_options
128
- auth_options = base_options.select { |k,v| auth_option_keys.include? k }
129
-
130
- use Signet::Rails::Handler, base_options, auth_options, &block
91
+ # Standard ::Rack::Builder initialize
92
+ #
93
+ # It is expected that after creating an instance of Signet::Rails::Builder one will call
94
+ # provider at least one in order to add an instance of the Handler Rack middleware to the
95
+ # Rack. e.g.
96
+ #
97
+ # class FakeApp < Sinatra::Base
98
+ # get "/" do
99
+ # "Hello"
100
+ # end
101
+ # end
102
+ #
103
+ # Signet::Rails::Builder.new FakeApp.new do
104
+ # provider name: :google...
105
+ # end
106
+ def initialize(app, &block)
107
+ super
131
108
  end
132
109
 
110
+ # Called when constructing the Rack stack
133
111
  def call(env)
134
- to_app.call(env)
112
+ to_app.call(env)
135
113
  end
114
+
115
+ # add an OAuth2 Handler to the initialize'ed app, defined by options
116
+ def provider(opts = {}, &block)
117
+ combined_options = OPTIONS.merge \
118
+ Builder.default_options.merge \
119
+ opts.symbolize_keys
120
+
121
+ provider_name = combined_options[:name]
122
+
123
+ # the following defaults depend on other defaults... hence are initialised here
124
+ # If customising either, clearly a similar effect can be achieved in the calling
125
+ # code or by passing in a &block which get invoked in Rack space (see the use call
126
+ # below)
127
+
128
+ # minor efficiency gain... only load the ActiveRecord persistance wrapper if the
129
+ # user hasn't set either extract_credentials_from_env or extract_credentials_from_env_by_oauth_id
130
+
131
+ unless combined_options[:extract_credentials_from_env] and combined_options[:extract_credentials_from_env_by_oauth_id]
132
+ require 'active_record'
133
+ require 'signet/rails/wrappers/active_record'
134
+ end
135
+
136
+ # TODO document
137
+ # Deliberately left verbose to give an example (although the code that follows
138
+ # could hardly be desribed as example) of what's required
139
+ combined_options[:extract_credentials_from_env] ||= lambda do |env, client|
140
+ oac = nil
141
+ session = env['rack.session']
142
+ if !!session && !!session[:user_id]
143
+ begin
144
+ u = ::User.find(session[:user_id])
145
+ oac = u.o_auth2_credentials.where(name: combined_options[:name]).first
146
+ rescue ::ActiveRecord::RecordNotFound => e
147
+ end
148
+ end
149
+ Signet::Rails::Wrappers::ActiveRecord.new oac, client
150
+ end
151
+
152
+ combined_options[:extract_credentials_from_env_by_oauth_id] ||= lambda do |env, client, id|
153
+ oac = nil
154
+ begin
155
+ u = nil
156
+ if combined_options[:type] == :login
157
+ u = ::User.where(uid: combined_options[:name].to_s + "_" + id).first_or_create
158
+ else
159
+ session = env['rack.session']
160
+ if !!session && !!session[:user_id]
161
+ begin
162
+ u = ::User.find(session[:user_id])
163
+ rescue ::ActiveRecord::RecordNotFound => e
164
+ end
165
+ else
166
+ raise "Expected to be able to find user in session"
167
+ end
168
+ end
169
+
170
+ oac = u.o_auth2_credentials.where(name: combined_options[:name]).first_or_initialize
171
+
172
+ rescue ::ActiveRecord::RecordNotFound => e
173
+ # TODO
174
+ end
175
+
176
+ Signet::Rails::Wrappers::ActiveRecord.new oac, client
177
+ end
178
+
179
+ # TODO: check here we have the basics?
180
+
181
+ # TODO: better auth_options split?
182
+ auth_option_keys = [:prompt, :redirect_uri, :approval_prompt, :client_id]
183
+ auth_options = combined_options.slice(*auth_option_keys)
184
+
185
+ # verify we have certain required values
186
+ if combined_options[:type] == :login
187
+ raise ArgumentError, 'Client id is required for a type: :login provider' unless auth_options[:client_id] and auth_options[:client_id].is_a? String
188
+ raise ArgumentError, 'Scope is required' unless combined_options[:scope]
189
+ # TODO error handling for scope: must be a string or array of strings
190
+ end
191
+
192
+ use Signet::Rails::Handler, combined_options, auth_options, &block
193
+ end
194
+
136
195
  end
137
196
  end
138
197
  end
@@ -1,32 +1,63 @@
1
- require 'signet/oauth_2'
1
+ require 'signet/oauth_2/client'
2
2
 
3
3
  module Signet
4
4
  module Rails
5
5
 
6
+ # Factory for creating instances of the Signet::OAuth2::Client or extracting
7
+ # them from an existing env
8
+ #
9
+ # There are two use cases:
10
+ #
11
+ # 1. This is called from within the user app (e.g. Rails) - in this case we
12
+ # extract an existing instance from the env provided by the Handler Rack
13
+ # middleware. We then load the persisted token information (if there is any)
14
+ # and set that on the client. The client is then returned.
15
+ #
16
+ # name: the provider name
17
+ # env: the env available to the caller e.g. request.env in rails
18
+ # options: left blank
19
+ #
20
+ # 2. This is called from within the Handler Rack Middleware (as described above)
21
+ # In this case, we have have to construct the client instance from the Handler
22
+ # options (previously configured)
23
+ #
24
+ # name: the provider name
25
+ # env: the env available to the caller e.g. request.env in rails, the env
26
+ # from the previous Rack middleware in the case of a Handler
27
+ # options: load_token: false
28
+ #
29
+ # In either case, we can rely on env["signet.<HANDLER_NAME>"] being set at the
30
+ # point of calling
6
31
  class Factory
7
- def self.create_from_env name, env, opt_hsh = {load_token: true}
32
+
33
+ def self.create_from_env(name, env, options = { load_token: true })
34
+ # TODO: checking of env not pretty...thread safe? best approach?
8
35
 
9
- # TODO: not pretty...thread safe? best approach? Other uses below
10
- handler = env["signet.#{name.to_s}"]
11
- instance = env["signet.#{name.to_s}.instance"]
12
-
13
- #client = instance.obj
14
- client = instance
36
+ # If there is already an instance in this env, return it
37
+ env["signet.#{name}.instance"] ||
38
+ # else create one from the handler
39
+ create_client_from_handler(env["signet.#{name}"], name, env, options)
40
+ end
15
41
 
16
- return client if !!client
42
+ private
17
43
 
18
- if handler.nil?
19
- raise ArgumentError, "Unable to find signet handler named #{name.to_s}"
20
- end
44
+ def self.create_client_from_handler(handler, name, env, options)
45
+ # There should be a handler set at this stage... big problems otherwise
46
+ raise ArgumentError, "Unable to find signet handler named #{name}" unless handler
21
47
 
22
48
  client = Signet::OAuth2::Client.new handler.options
23
49
 
24
- if opt_hsh[:load_token]
25
- obj = handler.options[:extract_from_env].call env, client
50
+ # at this point we have satisfied use case 2
51
+
52
+ # here is use case 1; the client we have created thus far is blank as far
53
+ # as access/refresh tokens are concerned. If we are calling from a user app
54
+ # then we want that loaded (for the current user)
55
+ if options[:load_token]
56
+ obj = handler.options[:extract_credentials_from_env].call env, client
26
57
  handler.load_token_state obj, client
27
58
 
28
- #instance.obj = obj
29
- env["signet.#{name.to_s}.instance"] = obj
59
+ # store the created instance in the env for future use/persistance
60
+ env["signet.#{name}.instance"] = obj
30
61
  end
31
62
 
32
63
  client
@@ -4,122 +4,182 @@ require 'rack/utils'
4
4
 
5
5
  module Signet
6
6
  module Rails
7
- class Handler
7
+
8
+ class Handler # this is the Rack middleware - it responds to call(env)
9
+
10
+ # we are initialized here with the app that is above us in the stack
11
+ # i.e. where we relay the request (unless handled)
8
12
  def initialize(app, opts = {}, auth_opts = {}, &block)
9
- @app = app
10
- @options = opts
11
- @auth_options = auth_opts
13
+ @app = app
14
+ @options = opts
15
+ @auth_options = auth_opts
12
16
  end
13
17
 
14
- def options
15
- # TODO: this is because signet doesn't dup what we pass in....
16
- @options.dup
18
+ # The Rack entry point. This method gets called from the Rack middleware below us
19
+ # in the stack. If we need to handle the request we respond else we pass the request
20
+ # up the stack and then persist the oauth tokens
21
+ def call(env)
22
+
23
+ # Make this Handler available to the env
24
+ # TODO rework this to use singleton?
25
+ env["signet.#{options[:name]}"] = self
26
+
27
+ status, headers, body = nil, nil, nil
28
+
29
+ # TODO: better way than a gross if elsif block?
30
+ #
31
+ # There are two requests which we do something with:
32
+ #
33
+ # 1. on_auth_path? - a request to auth against a particular
34
+ # provider. We respond to this by redirecting to the provider's
35
+ # authorization_uri, translating appropriate options
36
+ #
37
+ # 2. on_auth_callback_path? - the callback from the provider's
38
+ # authorization_uri (ultimately) which will have an authorisation
39
+ # code. We respond to this by extracting the code, getting an
40
+ # access token (and response token), persisting said tokens then
41
+ # forwarding the request to the next Rack app up the stack. When
42
+ # the Rack app above us responds we parse the env to extract
43
+ # the potentially modified TODO finish
44
+ if on_auth_path?(env)
45
+ status, headers, body = create_auth_redirect_response env
46
+ else
47
+ if on_auth_callback_path?(env)
48
+ process_auth_callback env
49
+ end
50
+
51
+ status, headers, body = @app.call(env)
52
+
53
+ # did we create an instance during the process of @app.call?
54
+ # if so persist
55
+ instance = env["signet.#{options[:name]}.instance"]
56
+ if instance
57
+ save_token_state instance, instance.client
58
+ instance.persist
59
+ end
60
+ end
61
+
62
+ # notice here that only on_auth_path?(env) will ensure we respond
63
+ # otherwise status will be nil
64
+ [status, headers, body]
17
65
  end
18
66
 
19
- def auth_options env
20
- # TODO: this is because signet doesn't dup what we pass in....
21
- ret = @auth_options.dup
22
- unless ret.include? :redirect_uri
23
- req = Rack::Request.new env
24
- scheme = req.ssl? ? 'https' : 'http'
25
- ret[:redirect_uri] = "#{scheme}://#{req.host_with_port}/signet/#{options[:name]}/auth_callback"
26
- end
27
- ret
67
+
68
+ # Take a persistence wrapper object and a Signet::OAuth2Client
69
+ # and transfer the :persist_attrs to the persistence wrapper
70
+ def save_token_state wrapper, client
71
+ if not wrapper.credentials.respond_to?(options[:storage_attr])
72
+ raise "Persistence object does not support the storage attribute #{options[:storage_attr]}"
73
+ end
74
+
75
+ if (store_hash = wrapper.credentials.method(options[:storage_attr]).call).nil?
76
+ store_hash = wrapper.credentials.method("#{options[:storage_attr]}=").call({})
77
+ end
78
+
79
+ # not nice... the wrapper.credentials.changed? will only be triggered if we clone the hash
80
+ # Is this a bug? https://github.com/rails/rails/issues/11968
81
+ # TODO: check if there is a better solution
82
+ store_hash = store_hash.clone
83
+
84
+ for i in options[:persist_attrs]
85
+ if client.respond_to?(i)
86
+ # only transfer the value if it is non-nil
87
+ store_hash[i.to_s] = client.method(i).call unless client.method(i).call.nil?
88
+ end
89
+ end
90
+
91
+ wrapper.credentials.method("#{options[:storage_attr]}=").call(store_hash)
28
92
  end
29
93
 
30
- def handle(env)
31
-
32
- # set these to 'handle' the request
33
- status = headers = body = nil
34
-
35
- # TODO: better way than a gross if elsif block?
36
- if "/signet/#{options[:name]}/auth" == env['PATH_INFO'] && 'GET' == env['REQUEST_METHOD']
37
-
38
- # we are looking to auth... so nothing to load
39
- client = Factory.create_from_env options[:name], env, load_token: false
40
-
41
- r = Rack::Response.new
42
- redirect_uri = client.authorization_uri(auth_options(env)).to_s
43
- r.redirect(redirect_uri)
44
- status, headers, body = r.finish
45
- elsif "/signet/#{options[:name]}/auth_callback" == env['PATH_INFO'] && 'GET' == env['REQUEST_METHOD']
46
- client = Factory.create_from_env options[:name], env, load_token: false
47
- query_string_params = Rack::Utils.parse_query(env['QUERY_STRING'])
48
- client.code = query_string_params['code']
49
- client.redirect_uri = auth_options(env)[:redirect_uri]
50
- client.fetch_access_token!
51
-
52
- if options[:handle_auth_callback]
53
- obj = options[:extract_by_oauth_id].call env, client, client.decoded_id_token['sub']
54
- persist_token_state obj, client
55
- obj.persist
56
- env["signet.#{options[:name]}.persistence_obj"] = obj.obj
57
- else
58
- env["signet.#{options[:name]}.auth_client"] = client
59
- end
60
- end
61
-
62
- [status, headers, body]
94
+ # The inverse of save_token_state
95
+ def load_token_state wrapper, client
96
+ if not wrapper.credentials.respond_to?(options[:storage_attr])
97
+ raise "Persistence object does not support the storage attribute #{options[:storage_attr]}"
98
+ end
99
+
100
+ if not (store_hash = wrapper.credentials.method(options[:storage_attr]).call).nil?
101
+ for i in options[:persist_attrs]
102
+ if client.respond_to?(i.to_s+'=')
103
+ client.method(i.to_s+'=').call(store_hash[i.to_s])
104
+ end
105
+ end
106
+ end
63
107
  end
64
108
 
65
- def persist_token_state wrapper, client
66
- if not wrapper.obj.respond_to?(options[:storage_attr])
67
- raise "Persistence object does not support the storage attribute #{options[:storage_attr]}"
68
- end
69
-
70
- if (store_hash = wrapper.obj.method(options[:storage_attr]).call).nil?
71
- store_hash = wrapper.obj.method("#{options[:storage_attr]}=").call({})
72
- end
73
-
74
- # not nice... the wrapper.obj.changed? will only be triggered if we clone the hash
75
- # Is this a bug? https://github.com/rails/rails/issues/11968
76
- # TODO: check if there is a better solution
77
- store_hash = store_hash.clone
78
-
79
- for i in options[:persist_attrs]
80
- if client.respond_to?(i)
81
- # only transfer the value if it is non-nil
82
- store_hash[i.to_s] = client.method(i).call unless client.method(i).call.nil?
83
- end
84
- end
85
-
86
- wrapper.obj.method("#{options[:storage_attr]}=").call(store_hash)
87
-
109
+ # TODO the code separation here is not great. Refactoring so that
110
+ # we don't have to publicly expose our options would be best
111
+ def options
112
+ # TODO: Signet does not dup the options that are passed to it...
113
+ # hence 'our' value would be corrupted were it not dup'ed here
114
+ @options.dup
88
115
  end
89
116
 
90
- def load_token_state wrapper, client
91
- if not wrapper.obj.respond_to?(options[:storage_attr])
92
- raise "Persistence object does not support the storage attribute #{options[:storage_attr]}"
93
- end
94
-
95
- if not (store_hash = wrapper.obj.method(options[:storage_attr]).call).nil?
96
- for i in options[:persist_attrs]
97
- if client.respond_to?(i.to_s+'=')
98
- client.method(i.to_s+'=').call(store_hash[i.to_s])
99
- end
100
- end
101
- end
117
+ def auth_options(env)
118
+ # TODO: Signet does not dup the options that are passed to it...
119
+ # hence 'our' value would be corrupted were it not dup'ed here
120
+ ret = @auth_options.dup
121
+
122
+ # the redirect uri can't be set at config time (when we mount the
123
+ # Rack middleware) - it must be done at request time. TODO really?
124
+ # Build the redirect_uri from the env in which we are called
125
+ unless ret.include? :redirect_uri and not ret[:redirect_uri].nil?
126
+ req = Rack::Request.new env
127
+ scheme = req.ssl? ? 'https' : 'http'
128
+ ret[:redirect_uri] = "#{scheme}://#{req.host_with_port}/signet/#{options[:name]}/auth_callback"
129
+ end
130
+ ret
131
+ end
132
+
133
+ private
134
+
135
+ def create_auth_redirect_response(env)
136
+ # we build the redirect uri from an OAuth2 client
137
+ # clearly we don't need token state loaded... indeed
138
+ # it probably doesn't exist for this user if we're being
139
+ # asked to auth
140
+ client = Factory.create_from_env(options[:name], env, load_token: false)
141
+
142
+ response = Rack::Response.new
143
+
144
+ # if it needs reiterating... we can't set the redirect uri
145
+ # at config time because we don't have certain config
146
+ # available like server_name etc. This is available at request
147
+ # time (i.e. now)
148
+ redirect_uri = client.authorization_uri(auth_options(env)).to_s
149
+ response.redirect(redirect_uri)
150
+
151
+ # will return status, headers, body
152
+ response.finish
102
153
  end
103
154
 
104
- def call(env)
105
- # rework this to use singleton?
106
- env["signet.#{options[:name]}"] = self
155
+ def process_auth_callback(env)
107
156
 
108
- status, headers, body = handle(env)
157
+ client = Factory.create_from_env options[:name], env, load_token: false
158
+ query_string_params = Rack::Utils.parse_query(env['QUERY_STRING'])
159
+ client.code = query_string_params['code']
160
+ client.redirect_uri = auth_options(env)[:redirect_uri]
109
161
 
110
- unless status
162
+ raise ArgumentError, 'Missing authorization code in auth_callback' unless client.code
111
163
 
112
- status, headers, body = @app.call(env)
164
+ # TODO is there a better way of passing in a connection for testing?
165
+ client.fetch_access_token!({connection: options[:connection]})
113
166
 
114
- instance = env["signet.#{options[:name]}.instance"]
115
- if !!instance
116
- persist_token_state instance, instance.client
117
- instance.persist
118
- end
167
+ if options[:handle_auth_callback]
168
+ user_oauth_credentials = options[:extract_credentials_from_env_by_oauth_id].call env, client, client.decoded_id_token['sub']
169
+ save_token_state user_oauth_credentials, client
170
+ user_oauth_credentials.persist
171
+ env["signet.#{options[:name]}.persistence_obj"] = user_oauth_credentials.credentials
172
+ else
173
+ env["signet.#{options[:name]}.auth_client"] = client
174
+ end
175
+ end
119
176
 
120
- end
177
+ def on_auth_path?(env)
178
+ "/signet/#{options[:name]}/auth" == env['PATH_INFO'] && 'GET' == env['REQUEST_METHOD']
179
+ end
121
180
 
122
- [status, headers, body]
181
+ def on_auth_callback_path?(env)
182
+ "/signet/#{options[:name]}/auth_callback" == env['PATH_INFO'] && 'GET' == env['REQUEST_METHOD']
123
183
  end
124
184
  end
125
185
  end