googleauth 1.3.0 → 1.13.1
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 +4 -4
- data/CHANGELOG.md +140 -0
- data/README.md +52 -2
- data/lib/googleauth/application_default.rb +5 -9
- data/lib/googleauth/base_client.rb +92 -0
- data/lib/googleauth/client_id.rb +25 -8
- data/lib/googleauth/compute_engine.rb +183 -39
- data/lib/googleauth/credentials.rb +169 -57
- data/lib/googleauth/credentials_loader.rb +5 -13
- data/lib/googleauth/default_credentials.rb +17 -3
- data/lib/googleauth/external_account/aws_credentials.rb +378 -0
- data/lib/googleauth/external_account/base_credentials.rb +190 -0
- data/lib/googleauth/external_account/external_account_utils.rb +103 -0
- data/lib/googleauth/external_account/identity_pool_credentials.rb +118 -0
- data/lib/googleauth/external_account/pluggable_credentials.rb +156 -0
- data/lib/googleauth/external_account.rb +94 -0
- data/lib/googleauth/helpers/connection.rb +35 -0
- data/lib/googleauth/id_tokens.rb +2 -4
- data/lib/googleauth/impersonated_service_account.rb +282 -0
- data/lib/googleauth/json_key_reader.rb +2 -1
- data/lib/googleauth/oauth2/sts_client.rb +109 -0
- data/lib/googleauth/scope_util.rb +35 -2
- data/lib/googleauth/service_account.rb +138 -17
- data/lib/googleauth/signet.rb +148 -37
- data/lib/googleauth/token_store.rb +3 -3
- data/lib/googleauth/user_authorizer.rb +66 -9
- data/lib/googleauth/user_refresh.rb +47 -2
- data/lib/googleauth/version.rb +1 -1
- data/lib/googleauth/web_user_authorizer.rb +19 -8
- metadata +44 -23
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
+
require "google/logging/message"
|
|
15
16
|
require "googleauth/signet"
|
|
16
17
|
require "googleauth/credentials_loader"
|
|
17
18
|
require "googleauth/json_key_reader"
|
|
@@ -39,7 +40,11 @@ module Google
|
|
|
39
40
|
attr_reader :quota_project_id
|
|
40
41
|
|
|
41
42
|
def enable_self_signed_jwt?
|
|
42
|
-
|
|
43
|
+
# Use a self-singed JWT if there's no information that can be used to
|
|
44
|
+
# obtain an OAuth token, OR if there are scopes but also an assertion
|
|
45
|
+
# that they are default scopes that shouldn't be used to fetch a token,
|
|
46
|
+
# OR we are not in the default universe and thus OAuth isn't supported.
|
|
47
|
+
target_audience.nil? && (scope.nil? || @enable_self_signed_jwt || universe_domain != "googleapis.com")
|
|
43
48
|
end
|
|
44
49
|
|
|
45
50
|
# Creates a ServiceAccountCredentials.
|
|
@@ -53,12 +58,13 @@ module Google
|
|
|
53
58
|
raise ArgumentError, "Cannot specify both scope and target_audience" if scope && target_audience
|
|
54
59
|
|
|
55
60
|
if json_key_io
|
|
56
|
-
private_key, client_email, project_id, quota_project_id = read_json_key json_key_io
|
|
61
|
+
private_key, client_email, project_id, quota_project_id, universe_domain = read_json_key json_key_io
|
|
57
62
|
else
|
|
58
63
|
private_key = unescape ENV[CredentialsLoader::PRIVATE_KEY_VAR]
|
|
59
64
|
client_email = ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
|
|
60
65
|
project_id = ENV[CredentialsLoader::PROJECT_ID_VAR]
|
|
61
66
|
quota_project_id = nil
|
|
67
|
+
universe_domain = nil
|
|
62
68
|
end
|
|
63
69
|
project_id ||= CredentialsLoader.load_gcloud_project_id
|
|
64
70
|
|
|
@@ -70,10 +76,35 @@ module Google
|
|
|
70
76
|
issuer: client_email,
|
|
71
77
|
signing_key: OpenSSL::PKey::RSA.new(private_key),
|
|
72
78
|
project_id: project_id,
|
|
73
|
-
quota_project_id: quota_project_id
|
|
79
|
+
quota_project_id: quota_project_id,
|
|
80
|
+
universe_domain: universe_domain || "googleapis.com")
|
|
74
81
|
.configure_connection(options)
|
|
75
82
|
end
|
|
76
83
|
|
|
84
|
+
# Creates a duplicate of these credentials
|
|
85
|
+
# without the Signet::OAuth2::Client-specific
|
|
86
|
+
# transient state (e.g. cached tokens)
|
|
87
|
+
#
|
|
88
|
+
# @param options [Hash] Overrides for the credentials parameters.
|
|
89
|
+
# The following keys are recognized in addition to keys in the
|
|
90
|
+
# Signet::OAuth2::Client
|
|
91
|
+
# * `:enable_self_signed_jwt` Whether the self-signed JWT should
|
|
92
|
+
# be used for the authentication
|
|
93
|
+
# * `project_id` the project id to use during the authentication
|
|
94
|
+
# * `quota_project_id` the quota project id to use
|
|
95
|
+
# during the authentication
|
|
96
|
+
def duplicate options = {}
|
|
97
|
+
options = deep_hash_normalize options
|
|
98
|
+
super(
|
|
99
|
+
{
|
|
100
|
+
enable_self_signed_jwt: @enable_self_signed_jwt,
|
|
101
|
+
project_id: project_id,
|
|
102
|
+
quota_project_id: quota_project_id,
|
|
103
|
+
logger: logger
|
|
104
|
+
}.merge(options)
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
77
108
|
# Handles certain escape sequences that sometimes appear in input.
|
|
78
109
|
# Specifically, interprets the "\n" sequence for newline, and removes
|
|
79
110
|
# enclosing quotes.
|
|
@@ -93,16 +124,44 @@ module Google
|
|
|
93
124
|
# Extends the base class to use a transient
|
|
94
125
|
# ServiceAccountJwtHeaderCredentials for certain cases.
|
|
95
126
|
def apply! a_hash, opts = {}
|
|
96
|
-
|
|
97
|
-
# obtain an OAuth token, OR if there are scopes but also an assertion
|
|
98
|
-
# that they are default scopes that shouldn't be used to fetch a token.
|
|
99
|
-
if target_audience.nil? && (scope.nil? || enable_self_signed_jwt?)
|
|
127
|
+
if enable_self_signed_jwt?
|
|
100
128
|
apply_self_signed_jwt! a_hash
|
|
101
129
|
else
|
|
102
130
|
super
|
|
103
131
|
end
|
|
104
132
|
end
|
|
105
133
|
|
|
134
|
+
# Modifies this logic so it also requires self-signed-jwt to be disabled
|
|
135
|
+
def needs_access_token?
|
|
136
|
+
super && !enable_self_signed_jwt?
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Destructively updates these credentials
|
|
140
|
+
#
|
|
141
|
+
# This method is called by `Signet::OAuth2::Client`'s constructor
|
|
142
|
+
#
|
|
143
|
+
# @param options [Hash] Overrides for the credentials parameters.
|
|
144
|
+
# The following keys are recognized in addition to keys in the
|
|
145
|
+
# Signet::OAuth2::Client
|
|
146
|
+
# * `:enable_self_signed_jwt` Whether the self-signed JWT should
|
|
147
|
+
# be used for the authentication
|
|
148
|
+
# * `project_id` the project id to use during the authentication
|
|
149
|
+
# * `quota_project_id` the quota project id to use
|
|
150
|
+
# during the authentication
|
|
151
|
+
# @return [Google::Auth::ServiceAccountCredentials]
|
|
152
|
+
def update! options = {}
|
|
153
|
+
# Normalize all keys to symbols to allow indifferent access.
|
|
154
|
+
options = deep_hash_normalize options
|
|
155
|
+
|
|
156
|
+
@enable_self_signed_jwt = options[:enable_self_signed_jwt] ? true : false
|
|
157
|
+
@project_id = options[:project_id] if options.key? :project_id
|
|
158
|
+
@quota_project_id = options[:quota_project_id] if options.key? :quota_project_id
|
|
159
|
+
|
|
160
|
+
super(options)
|
|
161
|
+
|
|
162
|
+
self
|
|
163
|
+
end
|
|
164
|
+
|
|
106
165
|
private
|
|
107
166
|
|
|
108
167
|
def apply_self_signed_jwt! a_hash
|
|
@@ -115,6 +174,7 @@ module Google
|
|
|
115
174
|
}
|
|
116
175
|
key_io = StringIO.new MultiJson.dump(cred_json)
|
|
117
176
|
alt = ServiceAccountJwtHeaderCredentials.make_creds json_key_io: key_io, scope: scope
|
|
177
|
+
alt.logger = logger
|
|
118
178
|
alt.apply! a_hash
|
|
119
179
|
end
|
|
120
180
|
end
|
|
@@ -130,14 +190,18 @@ module Google
|
|
|
130
190
|
# cf [Application Default Credentials](https://cloud.google.com/docs/authentication/production)
|
|
131
191
|
class ServiceAccountJwtHeaderCredentials
|
|
132
192
|
JWT_AUD_URI_KEY = :jwt_aud_uri
|
|
133
|
-
AUTH_METADATA_KEY =
|
|
193
|
+
AUTH_METADATA_KEY = Google::Auth::BaseClient::AUTH_METADATA_KEY
|
|
134
194
|
TOKEN_CRED_URI = "https://www.googleapis.com/oauth2/v4/token".freeze
|
|
135
195
|
SIGNING_ALGORITHM = "RS256".freeze
|
|
136
196
|
EXPIRY = 60
|
|
197
|
+
|
|
137
198
|
extend CredentialsLoader
|
|
138
199
|
extend JsonKeyReader
|
|
200
|
+
|
|
139
201
|
attr_reader :project_id
|
|
140
202
|
attr_reader :quota_project_id
|
|
203
|
+
attr_accessor :universe_domain
|
|
204
|
+
attr_accessor :logger
|
|
141
205
|
|
|
142
206
|
# Create a ServiceAccountJwtHeaderCredentials.
|
|
143
207
|
#
|
|
@@ -154,17 +218,46 @@ module Google
|
|
|
154
218
|
def initialize options = {}
|
|
155
219
|
json_key_io = options[:json_key_io]
|
|
156
220
|
if json_key_io
|
|
157
|
-
@private_key, @issuer, @project_id, @quota_project_id =
|
|
221
|
+
@private_key, @issuer, @project_id, @quota_project_id, @universe_domain =
|
|
158
222
|
self.class.read_json_key json_key_io
|
|
159
223
|
else
|
|
160
|
-
@private_key = ENV[CredentialsLoader::PRIVATE_KEY_VAR]
|
|
161
|
-
@issuer = ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
|
|
162
|
-
@project_id = ENV[CredentialsLoader::PROJECT_ID_VAR]
|
|
163
|
-
@quota_project_id =
|
|
224
|
+
@private_key = options.key?(:private_key) ? options[:private_key] : ENV[CredentialsLoader::PRIVATE_KEY_VAR]
|
|
225
|
+
@issuer = options.key?(:issuer) ? options[:issuer] : ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
|
|
226
|
+
@project_id = options.key?(:project_id) ? options[:project_id] : ENV[CredentialsLoader::PROJECT_ID_VAR]
|
|
227
|
+
@quota_project_id = options[:quota_project_id] if options.key? :quota_project_id
|
|
228
|
+
@universe_domain = options[:universe_domain] if options.key? :universe_domain
|
|
164
229
|
end
|
|
230
|
+
@universe_domain ||= "googleapis.com"
|
|
165
231
|
@project_id ||= CredentialsLoader.load_gcloud_project_id
|
|
166
232
|
@signing_key = OpenSSL::PKey::RSA.new @private_key
|
|
167
|
-
@scope = options[:scope]
|
|
233
|
+
@scope = options[:scope] if options.key? :scope
|
|
234
|
+
@logger = options[:logger] if options.key? :scope
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Creates a duplicate of these credentials
|
|
238
|
+
#
|
|
239
|
+
# @param options [Hash] Overrides for the credentials parameters.
|
|
240
|
+
# The following keys are recognized
|
|
241
|
+
# * `private key` the private key in string form
|
|
242
|
+
# * `issuer` the SA issuer
|
|
243
|
+
# * `scope` the scope(s) to access
|
|
244
|
+
# * `project_id` the project id to use during the authentication
|
|
245
|
+
# * `quota_project_id` the quota project id to use
|
|
246
|
+
# * `universe_domain` the universe domain of the credentials
|
|
247
|
+
def duplicate options = {}
|
|
248
|
+
options = deep_hash_normalize options
|
|
249
|
+
|
|
250
|
+
options = {
|
|
251
|
+
private_key: @private_key,
|
|
252
|
+
issuer: @issuer,
|
|
253
|
+
scope: @scope,
|
|
254
|
+
project_id: project_id,
|
|
255
|
+
quota_project_id: quota_project_id,
|
|
256
|
+
universe_domain: universe_domain,
|
|
257
|
+
logger: logger
|
|
258
|
+
}.merge(options)
|
|
259
|
+
|
|
260
|
+
self.class.new options
|
|
168
261
|
end
|
|
169
262
|
|
|
170
263
|
# Construct a jwt token if the JWT_AUD_URI key is present in the input
|
|
@@ -176,10 +269,14 @@ module Google
|
|
|
176
269
|
return a_hash if jwt_aud_uri.nil? && @scope.nil?
|
|
177
270
|
jwt_token = new_jwt_token jwt_aud_uri, opts
|
|
178
271
|
a_hash[AUTH_METADATA_KEY] = "Bearer #{jwt_token}"
|
|
272
|
+
logger&.debug do
|
|
273
|
+
hash = Digest::SHA256.hexdigest jwt_token
|
|
274
|
+
Google::Logging::Message.from message: "Sending JWT auth token. (sha256:#{hash})"
|
|
275
|
+
end
|
|
179
276
|
a_hash
|
|
180
277
|
end
|
|
181
278
|
|
|
182
|
-
# Returns a clone of a_hash updated with the
|
|
279
|
+
# Returns a clone of a_hash updated with the authorization header
|
|
183
280
|
def apply a_hash, opts = {}
|
|
184
281
|
a_copy = a_hash.clone
|
|
185
282
|
apply! a_copy, opts
|
|
@@ -192,8 +289,6 @@ module Google
|
|
|
192
289
|
proc { |a_hash, opts = {}| apply a_hash, opts }
|
|
193
290
|
end
|
|
194
291
|
|
|
195
|
-
protected
|
|
196
|
-
|
|
197
292
|
# Creates a jwt uri token.
|
|
198
293
|
def new_jwt_token jwt_aud_uri = nil, options = {}
|
|
199
294
|
now = Time.new
|
|
@@ -210,8 +305,34 @@ module Google
|
|
|
210
305
|
assertion["scope"] = Array(@scope).join " " if @scope
|
|
211
306
|
assertion["aud"] = jwt_aud_uri if jwt_aud_uri
|
|
212
307
|
|
|
308
|
+
logger&.debug do
|
|
309
|
+
Google::Logging::Message.from message: "JWT assertion: #{assertion}"
|
|
310
|
+
end
|
|
311
|
+
|
|
213
312
|
JWT.encode assertion, @signing_key, SIGNING_ALGORITHM
|
|
214
313
|
end
|
|
314
|
+
|
|
315
|
+
# Duck-types the corresponding method from BaseClient
|
|
316
|
+
def needs_access_token?
|
|
317
|
+
false
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
private
|
|
321
|
+
|
|
322
|
+
def deep_hash_normalize old_hash
|
|
323
|
+
sym_hash = {}
|
|
324
|
+
old_hash&.each { |k, v| sym_hash[k.to_sym] = recursive_hash_normalize_keys v }
|
|
325
|
+
sym_hash
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Convert all keys in this hash (nested) to symbols for uniform retrieval
|
|
329
|
+
def recursive_hash_normalize_keys val
|
|
330
|
+
if val.is_a? Hash
|
|
331
|
+
deep_hash_normalize val
|
|
332
|
+
else
|
|
333
|
+
val
|
|
334
|
+
end
|
|
335
|
+
end
|
|
215
336
|
end
|
|
216
337
|
end
|
|
217
338
|
end
|
data/lib/googleauth/signet.rb
CHANGED
|
@@ -12,58 +12,61 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
+
require "base64"
|
|
16
|
+
require "json"
|
|
15
17
|
require "signet/oauth_2/client"
|
|
18
|
+
require "googleauth/base_client"
|
|
16
19
|
|
|
17
20
|
module Signet
|
|
18
21
|
# OAuth2 supports OAuth2 authentication.
|
|
19
22
|
module OAuth2
|
|
20
|
-
AUTH_METADATA_KEY = :authorization
|
|
21
23
|
# Signet::OAuth2::Client creates an OAuth2 client
|
|
22
24
|
#
|
|
23
25
|
# This reopens Client to add #apply and #apply! methods which update a
|
|
24
26
|
# hash with the fetched authentication token.
|
|
25
27
|
class Client
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
include Google::Auth::BaseClient
|
|
29
|
+
|
|
30
|
+
alias update_token_signet_base update_token!
|
|
31
|
+
|
|
32
|
+
def update_token! options = {}
|
|
33
|
+
options = deep_hash_normalize options
|
|
34
|
+
id_token_expires_at = expires_at_from_id_token options[:id_token]
|
|
35
|
+
options[:expires_at] = id_token_expires_at if id_token_expires_at
|
|
36
|
+
update_token_signet_base options
|
|
37
|
+
self.universe_domain = options[:universe_domain] if options.key? :universe_domain
|
|
29
38
|
self
|
|
30
39
|
end
|
|
31
40
|
|
|
32
|
-
|
|
33
|
-
def
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
alias update_signet_base update!
|
|
42
|
+
def update! options = {}
|
|
43
|
+
# Normalize all keys to symbols to allow indifferent access.
|
|
44
|
+
options = deep_hash_normalize options
|
|
36
45
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
# This `update!` method "overide" adds the `@logger`` update and
|
|
47
|
+
# the `universe_domain` update.
|
|
48
|
+
#
|
|
49
|
+
# The `universe_domain` is also updated in `update_token!` but is
|
|
50
|
+
# included here for completeness
|
|
51
|
+
self.universe_domain = options[:universe_domain] if options.key? :universe_domain
|
|
52
|
+
@logger = options[:logger] if options.key? :logger
|
|
41
53
|
|
|
42
|
-
|
|
43
|
-
def apply! a_hash, opts = {}
|
|
44
|
-
# fetch the access token there is currently not one, or if the client
|
|
45
|
-
# has expired
|
|
46
|
-
fetch_access_token! opts if needs_access_token?
|
|
47
|
-
a_hash[AUTH_METADATA_KEY] = "Bearer #{send token_type}"
|
|
54
|
+
update_signet_base options
|
|
48
55
|
end
|
|
49
56
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
a_copy
|
|
57
|
+
def configure_connection options
|
|
58
|
+
@connection_info =
|
|
59
|
+
options[:connection_builder] || options[:default_connection]
|
|
60
|
+
self
|
|
55
61
|
end
|
|
56
62
|
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
proc { |a_hash, opts = {}| apply a_hash, opts }
|
|
63
|
+
# The token type as symbol, either :id_token or :access_token
|
|
64
|
+
def token_type
|
|
65
|
+
target_audience ? :id_token : :access_token
|
|
61
66
|
end
|
|
62
67
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
@refresh_listeners << block
|
|
66
|
-
end
|
|
68
|
+
# Set the universe domain
|
|
69
|
+
attr_accessor :universe_domain
|
|
67
70
|
|
|
68
71
|
alias orig_fetch_access_token! fetch_access_token!
|
|
69
72
|
def fetch_access_token! options = {}
|
|
@@ -78,11 +81,22 @@ module Signet
|
|
|
78
81
|
info
|
|
79
82
|
end
|
|
80
83
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
alias googleauth_orig_generate_access_token_request generate_access_token_request
|
|
85
|
+
def generate_access_token_request options = {}
|
|
86
|
+
parameters = googleauth_orig_generate_access_token_request options
|
|
87
|
+
logger&.info do
|
|
88
|
+
Google::Logging::Message.from(
|
|
89
|
+
message: "Requesting access token from #{parameters['grant_type']}",
|
|
90
|
+
"credentialsId" => object_id
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
logger&.debug do
|
|
94
|
+
Google::Logging::Message.from(
|
|
95
|
+
message: "Token fetch params: #{parameters}",
|
|
96
|
+
"credentialsId" => object_id
|
|
97
|
+
)
|
|
85
98
|
end
|
|
99
|
+
parameters
|
|
86
100
|
end
|
|
87
101
|
|
|
88
102
|
def build_default_connection
|
|
@@ -99,20 +113,117 @@ module Signet
|
|
|
99
113
|
retry_count = 0
|
|
100
114
|
|
|
101
115
|
begin
|
|
102
|
-
yield
|
|
116
|
+
yield.tap { |resp| log_response resp }
|
|
103
117
|
rescue StandardError => e
|
|
104
|
-
|
|
118
|
+
if e.is_a?(Signet::AuthorizationError) || e.is_a?(Signet::ParseError)
|
|
119
|
+
log_auth_error e
|
|
120
|
+
raise e
|
|
121
|
+
end
|
|
105
122
|
|
|
106
123
|
if retry_count < max_retry_count
|
|
124
|
+
log_transient_error e
|
|
107
125
|
retry_count += 1
|
|
108
126
|
sleep retry_count * 0.3
|
|
109
127
|
retry
|
|
110
128
|
else
|
|
129
|
+
log_retries_exhausted e
|
|
111
130
|
msg = "Unexpected error: #{e.inspect}"
|
|
112
131
|
raise Signet::AuthorizationError, msg
|
|
113
132
|
end
|
|
114
133
|
end
|
|
115
134
|
end
|
|
135
|
+
|
|
136
|
+
# Creates a duplicate of these credentials
|
|
137
|
+
# without the Signet::OAuth2::Client-specific
|
|
138
|
+
# transient state (e.g. cached tokens)
|
|
139
|
+
#
|
|
140
|
+
# @param options [Hash] Overrides for the credentials parameters.
|
|
141
|
+
# @see Signet::OAuth2::Client#update!
|
|
142
|
+
def duplicate options = {}
|
|
143
|
+
options = deep_hash_normalize options
|
|
144
|
+
|
|
145
|
+
opts = {
|
|
146
|
+
authorization_uri: @authorization_uri,
|
|
147
|
+
token_credential_uri: @token_credential_uri,
|
|
148
|
+
client_id: @client_id,
|
|
149
|
+
client_secret: @client_secret,
|
|
150
|
+
scope: @scope,
|
|
151
|
+
target_audience: @target_audience,
|
|
152
|
+
redirect_uri: @redirect_uri,
|
|
153
|
+
username: @username,
|
|
154
|
+
password: @password,
|
|
155
|
+
issuer: @issuer,
|
|
156
|
+
person: @person,
|
|
157
|
+
sub: @sub,
|
|
158
|
+
audience: @audience,
|
|
159
|
+
signing_key: @signing_key,
|
|
160
|
+
extension_parameters: @extension_parameters,
|
|
161
|
+
additional_parameters: @additional_parameters,
|
|
162
|
+
access_type: @access_type,
|
|
163
|
+
universe_domain: @universe_domain,
|
|
164
|
+
logger: @logger
|
|
165
|
+
}.merge(options)
|
|
166
|
+
|
|
167
|
+
new_client = self.class.new opts
|
|
168
|
+
|
|
169
|
+
new_client.configure_connection options
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
private
|
|
173
|
+
|
|
174
|
+
def expires_at_from_id_token id_token
|
|
175
|
+
match = /^[\w=-]+\.([\w=-]+)\.[\w=-]+$/.match id_token.to_s
|
|
176
|
+
return unless match
|
|
177
|
+
json = JSON.parse Base64.urlsafe_decode64 match[1]
|
|
178
|
+
return unless json.key? "exp"
|
|
179
|
+
Time.at json["exp"].to_i
|
|
180
|
+
rescue StandardError
|
|
181
|
+
# Shouldn't happen unless we get a garbled ID token
|
|
182
|
+
nil
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def log_response token_response
|
|
186
|
+
response_hash = JSON.parse token_response rescue {}
|
|
187
|
+
if response_hash["access_token"]
|
|
188
|
+
digest = Digest::SHA256.hexdigest response_hash["access_token"]
|
|
189
|
+
response_hash["access_token"] = "(sha256:#{digest})"
|
|
190
|
+
end
|
|
191
|
+
if response_hash["id_token"]
|
|
192
|
+
digest = Digest::SHA256.hexdigest response_hash["id_token"]
|
|
193
|
+
response_hash["id_token"] = "(sha256:#{digest})"
|
|
194
|
+
end
|
|
195
|
+
Google::Logging::Message.from(
|
|
196
|
+
message: "Received auth token response: #{response_hash}",
|
|
197
|
+
"credentialsId" => object_id
|
|
198
|
+
)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def log_auth_error err
|
|
202
|
+
logger&.info do
|
|
203
|
+
Google::Logging::Message.from(
|
|
204
|
+
message: "Auth error when fetching auth token: #{err}",
|
|
205
|
+
"credentialsId" => object_id
|
|
206
|
+
)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def log_transient_error err
|
|
211
|
+
logger&.info do
|
|
212
|
+
Google::Logging::Message.from(
|
|
213
|
+
message: "Transient error when fetching auth token: #{err}",
|
|
214
|
+
"credentialsId" => object_id
|
|
215
|
+
)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def log_retries_exhausted err
|
|
220
|
+
logger&.info do
|
|
221
|
+
Google::Logging::Message.from(
|
|
222
|
+
message: "Exhausted retries when fetching auth token: #{err}",
|
|
223
|
+
"credentialsId" => object_id
|
|
224
|
+
)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
116
227
|
end
|
|
117
228
|
end
|
|
118
229
|
end
|
|
@@ -29,7 +29,7 @@ module Google
|
|
|
29
29
|
# @return [String]
|
|
30
30
|
# The loaded token data.
|
|
31
31
|
def load _id
|
|
32
|
-
raise "
|
|
32
|
+
raise NoMethodError, "load not implemented"
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
# Put the token data into storage for the given ID.
|
|
@@ -39,7 +39,7 @@ module Google
|
|
|
39
39
|
# @param [String] token
|
|
40
40
|
# The token data to store.
|
|
41
41
|
def store _id, _token
|
|
42
|
-
raise "
|
|
42
|
+
raise NoMethodError, "store not implemented"
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
# Remove the token data from storage for the given ID.
|
|
@@ -47,7 +47,7 @@ module Google
|
|
|
47
47
|
# @param [String] id
|
|
48
48
|
# ID of the token data to delete
|
|
49
49
|
def delete _id
|
|
50
|
-
raise "
|
|
50
|
+
raise NoMethodError, "delete not implemented"
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
53
|
end
|
|
@@ -16,6 +16,7 @@ require "uri"
|
|
|
16
16
|
require "multi_json"
|
|
17
17
|
require "googleauth/signet"
|
|
18
18
|
require "googleauth/user_refresh"
|
|
19
|
+
require "securerandom"
|
|
19
20
|
|
|
20
21
|
module Google
|
|
21
22
|
module Auth
|
|
@@ -54,17 +55,26 @@ module Google
|
|
|
54
55
|
# Authorization scope to request
|
|
55
56
|
# @param [Google::Auth::Stores::TokenStore] token_store
|
|
56
57
|
# Backing storage for persisting user credentials
|
|
57
|
-
# @param [String]
|
|
58
|
+
# @param [String] legacy_callback_uri
|
|
58
59
|
# URL (either absolute or relative) of the auth callback.
|
|
59
|
-
# Defaults to '/oauth2callback'
|
|
60
|
-
|
|
60
|
+
# Defaults to '/oauth2callback'.
|
|
61
|
+
# @deprecated This field is deprecated. Instead, use the keyword
|
|
62
|
+
# argument callback_uri.
|
|
63
|
+
# @param [String] code_verifier
|
|
64
|
+
# Random string of 43-128 chars used to verify the key exchange using
|
|
65
|
+
# PKCE.
|
|
66
|
+
def initialize client_id, scope, token_store,
|
|
67
|
+
legacy_callback_uri = nil,
|
|
68
|
+
callback_uri: nil,
|
|
69
|
+
code_verifier: nil
|
|
61
70
|
raise NIL_CLIENT_ID_ERROR if client_id.nil?
|
|
62
71
|
raise NIL_SCOPE_ERROR if scope.nil?
|
|
63
72
|
|
|
64
73
|
@client_id = client_id
|
|
65
74
|
@scope = Array(scope)
|
|
66
75
|
@token_store = token_store
|
|
67
|
-
@callback_uri = callback_uri || "/oauth2callback"
|
|
76
|
+
@callback_uri = legacy_callback_uri || callback_uri || "/oauth2callback"
|
|
77
|
+
@code_verifier = code_verifier
|
|
68
78
|
end
|
|
69
79
|
|
|
70
80
|
# Build the URL for requesting authorization.
|
|
@@ -80,14 +90,29 @@ module Google
|
|
|
80
90
|
# @param [String, Array<String>] scope
|
|
81
91
|
# Authorization scope to request. Overrides the instance scopes if not
|
|
82
92
|
# nil.
|
|
93
|
+
# @param [Hash] additional_parameters
|
|
94
|
+
# Additional query parameters to be added to the authorization URL.
|
|
83
95
|
# @return [String]
|
|
84
96
|
# Authorization url
|
|
85
97
|
def get_authorization_url options = {}
|
|
86
98
|
scope = options[:scope] || @scope
|
|
99
|
+
|
|
100
|
+
options[:additional_parameters] ||= {}
|
|
101
|
+
|
|
102
|
+
if @code_verifier
|
|
103
|
+
options[:additional_parameters].merge!(
|
|
104
|
+
{
|
|
105
|
+
code_challenge: generate_code_challenge(@code_verifier),
|
|
106
|
+
code_challenge_method: code_challenge_method
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
|
|
87
111
|
credentials = UserRefreshCredentials.new(
|
|
88
112
|
client_id: @client_id.id,
|
|
89
113
|
client_secret: @client_id.secret,
|
|
90
|
-
scope: scope
|
|
114
|
+
scope: scope,
|
|
115
|
+
additional_parameters: options[:additional_parameters]
|
|
91
116
|
)
|
|
92
117
|
redirect_uri = redirect_uri_for options[:base_url]
|
|
93
118
|
url = credentials.authorization_uri(access_type: "offline",
|
|
@@ -144,6 +169,9 @@ module Google
|
|
|
144
169
|
# Absolute URL to resolve the configured callback uri against.
|
|
145
170
|
# Required if the configured
|
|
146
171
|
# callback uri is a relative.
|
|
172
|
+
# @param [Hash] additional_parameters
|
|
173
|
+
# Additional parameters to be added to the post body of token
|
|
174
|
+
# endpoint request.
|
|
147
175
|
# @return [Google::Auth::UserRefreshCredentials]
|
|
148
176
|
# Credentials if exchange is successful
|
|
149
177
|
def get_credentials_from_code options = {}
|
|
@@ -151,11 +179,14 @@ module Google
|
|
|
151
179
|
code = options[:code]
|
|
152
180
|
scope = options[:scope] || @scope
|
|
153
181
|
base_url = options[:base_url]
|
|
182
|
+
options[:additional_parameters] ||= {}
|
|
183
|
+
options[:additional_parameters].merge!({ code_verifier: @code_verifier })
|
|
154
184
|
credentials = UserRefreshCredentials.new(
|
|
155
|
-
client_id:
|
|
156
|
-
client_secret:
|
|
157
|
-
redirect_uri:
|
|
158
|
-
scope:
|
|
185
|
+
client_id: @client_id.id,
|
|
186
|
+
client_secret: @client_id.secret,
|
|
187
|
+
redirect_uri: redirect_uri_for(base_url),
|
|
188
|
+
scope: scope,
|
|
189
|
+
additional_parameters: options[:additional_parameters]
|
|
159
190
|
)
|
|
160
191
|
credentials.code = code
|
|
161
192
|
credentials.fetch_access_token!({})
|
|
@@ -221,6 +252,23 @@ module Google
|
|
|
221
252
|
credentials
|
|
222
253
|
end
|
|
223
254
|
|
|
255
|
+
# The code verifier for PKCE for OAuth 2.0. When set, the
|
|
256
|
+
# authorization URI will contain the Code Challenge and Code
|
|
257
|
+
# Challenge Method querystring parameters, and the token URI will
|
|
258
|
+
# contain the Code Verifier parameter.
|
|
259
|
+
#
|
|
260
|
+
# @param [String|nil] new_code_erifier
|
|
261
|
+
def code_verifier= new_code_verifier
|
|
262
|
+
@code_verifier = new_code_verifier
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Generate the code verifier needed to be sent while fetching
|
|
266
|
+
# authorization URL.
|
|
267
|
+
def self.generate_code_verifier
|
|
268
|
+
random_number = rand 32..96
|
|
269
|
+
SecureRandom.alphanumeric random_number
|
|
270
|
+
end
|
|
271
|
+
|
|
224
272
|
private
|
|
225
273
|
|
|
226
274
|
# @private Fetch stored token with given user_id
|
|
@@ -265,6 +313,15 @@ module Google
|
|
|
265
313
|
def uri_is_postmessage? uri
|
|
266
314
|
uri.to_s.casecmp("postmessage").zero?
|
|
267
315
|
end
|
|
316
|
+
|
|
317
|
+
def generate_code_challenge code_verifier
|
|
318
|
+
digest = Digest::SHA256.digest code_verifier
|
|
319
|
+
Base64.urlsafe_encode64 digest, padding: false
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def code_challenge_method
|
|
323
|
+
"S256"
|
|
324
|
+
end
|
|
268
325
|
end
|
|
269
326
|
end
|
|
270
327
|
end
|