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.
- checksums.yaml +7 -0
- data/.gitignore +2 -0
- data/.rspec +2 -0
- data/.rubocop.yml +5 -0
- data/lib/signet/rails/builder.rb +179 -120
- data/lib/signet/rails/factory.rb +47 -16
- data/lib/signet/rails/handler.rb +157 -97
- data/lib/signet/rails/version.rb +1 -1
- data/lib/signet/rails/wrappers/active_record.rb +5 -7
- data/signet-rails.gemspec +17 -10
- data/spec/factories/fakeapp_factory.rb +11 -0
- data/spec/factories/user_factory.rb +10 -0
- data/spec/handler_spec.rb +181 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/support/active_record.rb +45 -0
- metadata +129 -29
- data/tags +0 -37
checksums.yaml
ADDED
@@ -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
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/lib/signet/rails/builder.rb
CHANGED
@@ -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
|
-
|
10
|
-
|
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
|
-
|
16
|
-
|
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
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
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
|
data/lib/signet/rails/factory.rb
CHANGED
@@ -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
|
-
|
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
|
-
#
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
-
|
42
|
+
private
|
17
43
|
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
25
|
-
|
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
|
29
|
-
|
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
|
data/lib/signet/rails/handler.rb
CHANGED
@@ -4,122 +4,182 @@ require 'rack/utils'
|
|
4
4
|
|
5
5
|
module Signet
|
6
6
|
module Rails
|
7
|
-
|
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
|
-
|
10
|
-
|
11
|
-
|
13
|
+
@app = app
|
14
|
+
@options = opts
|
15
|
+
@auth_options = auth_opts
|
12
16
|
end
|
13
17
|
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
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
|
105
|
-
# rework this to use singleton?
|
106
|
-
env["signet.#{options[:name]}"] = self
|
155
|
+
def process_auth_callback(env)
|
107
156
|
|
108
|
-
|
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
|
-
|
162
|
+
raise ArgumentError, 'Missing authorization code in auth_callback' unless client.code
|
111
163
|
|
112
|
-
|
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
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
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
|
-
|
177
|
+
def on_auth_path?(env)
|
178
|
+
"/signet/#{options[:name]}/auth" == env['PATH_INFO'] && 'GET' == env['REQUEST_METHOD']
|
179
|
+
end
|
121
180
|
|
122
|
-
|
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
|