signet-rails 0.0.8 → 0.0.9
Sign up to get free protection for your applications and to get access to all the features.
- 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
|