googleauth 1.8.1 → 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 +82 -0
- data/README.md +48 -0
- data/lib/googleauth/application_default.rb +1 -5
- data/lib/googleauth/base_client.rb +16 -4
- data/lib/googleauth/compute_engine.rb +180 -44
- data/lib/googleauth/credentials.rb +169 -56
- data/lib/googleauth/default_credentials.rb +12 -1
- data/lib/googleauth/external_account/base_credentials.rb +37 -5
- data/lib/googleauth/external_account.rb +2 -1
- data/lib/googleauth/id_tokens.rb +0 -2
- data/lib/googleauth/impersonated_service_account.rb +282 -0
- data/lib/googleauth/json_key_reader.rb +2 -1
- data/lib/googleauth/service_account.rb +137 -14
- data/lib/googleauth/signet.rb +149 -2
- data/lib/googleauth/token_store.rb +3 -3
- data/lib/googleauth/user_authorizer.rb +54 -4
- data/lib/googleauth/user_refresh.rb +47 -2
- data/lib/googleauth/version.rb +1 -1
- data/lib/googleauth/web_user_authorizer.rb +15 -4
- metadata +35 -9
@@ -0,0 +1,282 @@
|
|
1
|
+
# Copyright 2024 Google, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require "googleauth/signet"
|
16
|
+
require "googleauth/base_client"
|
17
|
+
require "googleauth/helpers/connection"
|
18
|
+
|
19
|
+
module Google
|
20
|
+
module Auth
|
21
|
+
# Authenticates requests using impersonation from base credentials.
|
22
|
+
# This is a two-step process: first authentication claim from the base credentials is created
|
23
|
+
# and then that claim is exchanged for a short-lived token at an IAMCredentials endpoint.
|
24
|
+
# The short-lived token and its expiration time are cached.
|
25
|
+
class ImpersonatedServiceAccountCredentials
|
26
|
+
# @private
|
27
|
+
ERROR_SUFFIX = <<~ERROR.freeze
|
28
|
+
when trying to get security access token
|
29
|
+
from IAM Credentials endpoint using the credentials provided.
|
30
|
+
ERROR
|
31
|
+
|
32
|
+
# @private
|
33
|
+
IAM_SCOPE = ["https://www.googleapis.com/auth/iam".freeze].freeze
|
34
|
+
|
35
|
+
# BaseClient most importantly implements the `:updater_proc` getter,
|
36
|
+
# that returns a reference to an `apply!` method that updates
|
37
|
+
# a hash argument provided with the authorization header containing
|
38
|
+
# the access token (impersonation token in this case).
|
39
|
+
include Google::Auth::BaseClient
|
40
|
+
|
41
|
+
include Helpers::Connection
|
42
|
+
|
43
|
+
# @return [Object] The original authenticated credentials used to fetch short-lived impersonation access tokens
|
44
|
+
attr_reader :base_credentials
|
45
|
+
|
46
|
+
# @return [Object] The modified version of base credentials, tailored for impersonation purposes
|
47
|
+
# with necessary scope adjustments
|
48
|
+
attr_reader :source_credentials
|
49
|
+
|
50
|
+
# @return [String] The URL endpoint used to generate an impersonation token. This URL should follow a specific
|
51
|
+
# format to specify the impersonated service account.
|
52
|
+
attr_reader :impersonation_url
|
53
|
+
|
54
|
+
# @return [Array<String>, String] The scope(s) required for the impersonated access token,
|
55
|
+
# indicating the permissions needed for the short-lived token
|
56
|
+
attr_reader :scope
|
57
|
+
|
58
|
+
# @return [String, nil] The short-lived impersonation access token, retrieved and cached
|
59
|
+
# after making the impersonation request
|
60
|
+
attr_reader :access_token
|
61
|
+
|
62
|
+
# @return [Time, nil] The expiration time of the current access token, used to determine
|
63
|
+
# if the token is still valid
|
64
|
+
attr_reader :expires_at
|
65
|
+
|
66
|
+
# Create a ImpersonatedServiceAccountCredentials
|
67
|
+
# When you use service account impersonation, you start with an authenticated principal
|
68
|
+
# (e.g. your user account or a service account)
|
69
|
+
# and request short-lived credentials for a service account
|
70
|
+
# that has the authorization that your use case requires.
|
71
|
+
#
|
72
|
+
# @param options [Hash] A hash of options to configure the credentials.
|
73
|
+
# @option options [Object] :base_credentials (required) The authenticated principal.
|
74
|
+
# It will be used as following:
|
75
|
+
# * will be duplicated (with IAM scope) to create the source credentials if it supports duplication
|
76
|
+
# * as source credentials otherwise.
|
77
|
+
# @option options [String] :impersonation_url (required) The URL to impersonate the service account.
|
78
|
+
# This URL should follow the format:
|
79
|
+
# `https://iamcredentials.{universe_domain}/v1/projects/-/serviceAccounts/{source_sa_email}:generateAccessToken`,
|
80
|
+
# where:
|
81
|
+
# - `{universe_domain}` is the domain of the IAMCredentials API endpoint (e.g., `googleapis.com`).
|
82
|
+
# - `{source_sa_email}` is the email address of the service account to impersonate.
|
83
|
+
# @option options [Array<String>, String] :scope (required) The scope(s) for the short-lived impersonation token,
|
84
|
+
# defining the permissions required for the token.
|
85
|
+
# @option options [Object] :source_credentials The authenticated principal that will be used
|
86
|
+
# to fetch the short-lived impersonation access token. It is an alternative to providing the base credentials.
|
87
|
+
#
|
88
|
+
# @return [Google::Auth::ImpersonatedServiceAccountCredentials]
|
89
|
+
def self.make_creds options = {}
|
90
|
+
new options
|
91
|
+
end
|
92
|
+
|
93
|
+
# Initializes a new instance of ImpersonatedServiceAccountCredentials.
|
94
|
+
#
|
95
|
+
# @param options [Hash] A hash of options to configure the credentials.
|
96
|
+
# @option options [Object] :base_credentials (required) The authenticated principal.
|
97
|
+
# It will be used as following:
|
98
|
+
# * will be duplicated (with IAM scope) to create the source credentials if it supports duplication
|
99
|
+
# * as source credentials otherwise.
|
100
|
+
# @option options [String] :impersonation_url (required) The URL to impersonate the service account.
|
101
|
+
# This URL should follow the format:
|
102
|
+
# `https://iamcredentials.{universe_domain}/v1/projects/-/serviceAccounts/{source_sa_email}:generateAccessToken`,
|
103
|
+
# where:
|
104
|
+
# - `{universe_domain}` is the domain of the IAMCredentials API endpoint (e.g., `googleapis.com`).
|
105
|
+
# - `{source_sa_email}` is the email address of the service account to impersonate.
|
106
|
+
# @option options [Array<String>, String] :scope (required) The scope(s) for the short-lived impersonation token,
|
107
|
+
# defining the permissions required for the token.
|
108
|
+
# @option options [Object] :source_credentials The authenticated principal that will be used
|
109
|
+
# to fetch the short-lived impersonation access token. It is an alternative to providing the base credentials.
|
110
|
+
# It is redundant to provide both source and base credentials as only source will be used,
|
111
|
+
# but it can be done, e.g. when duplicating existing credentials.
|
112
|
+
#
|
113
|
+
# @raise [ArgumentError] If any of the required options are missing.
|
114
|
+
#
|
115
|
+
# @return [Google::Auth::ImpersonatedServiceAccountCredentials]
|
116
|
+
def initialize options = {}
|
117
|
+
@base_credentials, @impersonation_url, @scope =
|
118
|
+
options.values_at :base_credentials,
|
119
|
+
:impersonation_url,
|
120
|
+
:scope
|
121
|
+
|
122
|
+
# Fail-fast checks for required parameters
|
123
|
+
if @base_credentials.nil? && !options.key?(:source_credentials)
|
124
|
+
raise ArgumentError, "Missing required option: either :base_credentials or :source_credentials"
|
125
|
+
end
|
126
|
+
raise ArgumentError, "Missing required option: :impersonation_url" if @impersonation_url.nil?
|
127
|
+
raise ArgumentError, "Missing required option: :scope" if @scope.nil?
|
128
|
+
|
129
|
+
# Some credentials (all Signet-based ones and this one) include scope and a bunch of transient state
|
130
|
+
# (e.g. refresh status) as part of themselves
|
131
|
+
# so a copy needs to be created with the scope overriden and transient state dropped.
|
132
|
+
#
|
133
|
+
# If a credentials does not support `duplicate` we'll try to use it as is assuming it has a broad enough scope.
|
134
|
+
# This might result in an "access denied" error downstream when the token from that credentials is being used
|
135
|
+
# for the token exchange.
|
136
|
+
@source_credentials = if options.key? :source_credentials
|
137
|
+
options[:source_credentials]
|
138
|
+
elsif @base_credentials.respond_to? :duplicate
|
139
|
+
@base_credentials.duplicate({
|
140
|
+
scope: IAM_SCOPE
|
141
|
+
})
|
142
|
+
else
|
143
|
+
@base_credentials
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Determines whether the current access token expires within the specified number of seconds.
|
148
|
+
#
|
149
|
+
# @param seconds [Integer] The number of seconds to check against the token's expiration time.
|
150
|
+
#
|
151
|
+
# @return [Boolean] Whether the access token expires within the given time frame
|
152
|
+
def expires_within? seconds
|
153
|
+
# This method is needed for BaseClient
|
154
|
+
@expires_at && @expires_at - Time.now.utc < seconds
|
155
|
+
end
|
156
|
+
|
157
|
+
# The universe domain of the impersonated credentials.
|
158
|
+
# Effectively this retrieves the universe domain of the source credentials.
|
159
|
+
#
|
160
|
+
# @return [String] The universe domain of the credentials.
|
161
|
+
def universe_domain
|
162
|
+
@source_credentials.universe_domain
|
163
|
+
end
|
164
|
+
|
165
|
+
# @return [Logger, nil] The logger of the credentials.
|
166
|
+
def logger
|
167
|
+
@source_credentials.logger if source_credentials.respond_to? :logger
|
168
|
+
end
|
169
|
+
|
170
|
+
# Creates a duplicate of these credentials without transient token state
|
171
|
+
#
|
172
|
+
# @param options [Hash] Overrides for the credentials parameters.
|
173
|
+
# The following keys are recognized
|
174
|
+
# * `base_credentials` the base credentials used to initialize the impersonation
|
175
|
+
# * `source_credentials` the authenticated credentials which usually would be
|
176
|
+
# base credentials with scope overridden to IAM_SCOPE
|
177
|
+
# * `impersonation_url` the URL to use to make an impersonation token exchange
|
178
|
+
# * `scope` the scope(s) to access
|
179
|
+
#
|
180
|
+
# @return [Google::Auth::ImpersonatedServiceAccountCredentials]
|
181
|
+
def duplicate options = {}
|
182
|
+
options = deep_hash_normalize options
|
183
|
+
|
184
|
+
options = {
|
185
|
+
base_credentials: @base_credentials,
|
186
|
+
source_credentials: @source_credentials,
|
187
|
+
impersonation_url: @impersonation_url,
|
188
|
+
scope: @scope
|
189
|
+
}.merge(options)
|
190
|
+
|
191
|
+
self.class.new options
|
192
|
+
end
|
193
|
+
|
194
|
+
private
|
195
|
+
|
196
|
+
# Generates a new impersonation access token by exchanging the source credentials' token
|
197
|
+
# at the impersonation URL.
|
198
|
+
#
|
199
|
+
# This method first fetches an access token from the source credentials and then exchanges it
|
200
|
+
# for an impersonation token using the specified impersonation URL. The generated token and
|
201
|
+
# its expiration time are cached for subsequent use.
|
202
|
+
#
|
203
|
+
# @param _options [Hash] (optional) Additional options for token retrieval (currently unused).
|
204
|
+
#
|
205
|
+
# @raise [Signet::UnexpectedStatusError] If the response status is 403 or 500.
|
206
|
+
# @raise [Signet::AuthorizationError] For other unexpected response statuses.
|
207
|
+
#
|
208
|
+
# @return [String] The newly generated impersonation access token.
|
209
|
+
def fetch_access_token! _options = {}
|
210
|
+
auth_header = {}
|
211
|
+
auth_header = @source_credentials.updater_proc.call auth_header
|
212
|
+
|
213
|
+
resp = connection.post @impersonation_url do |req|
|
214
|
+
req.headers.merge! auth_header
|
215
|
+
req.headers["Content-Type"] = "application/json"
|
216
|
+
req.body = MultiJson.dump({ scope: @scope })
|
217
|
+
end
|
218
|
+
|
219
|
+
case resp.status
|
220
|
+
when 200
|
221
|
+
response = MultiJson.load resp.body
|
222
|
+
self.expires_at = response["expireTime"]
|
223
|
+
@access_token = response["accessToken"]
|
224
|
+
access_token
|
225
|
+
when 403, 500
|
226
|
+
msg = "Unexpected error code #{resp.status}.\n #{resp.env.response_body} #{ERROR_SUFFIX}"
|
227
|
+
raise Signet::UnexpectedStatusError, msg
|
228
|
+
else
|
229
|
+
msg = "Unexpected error code #{resp.status}.\n #{resp.env.response_body} #{ERROR_SUFFIX}"
|
230
|
+
raise Signet::AuthorizationError, msg
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# Setter for the expires_at value that makes sure it is converted
|
235
|
+
# to Time object.
|
236
|
+
def expires_at= new_expires_at
|
237
|
+
@expires_at = normalize_timestamp new_expires_at
|
238
|
+
end
|
239
|
+
|
240
|
+
# Returns the type of token (access_token).
|
241
|
+
# This method is needed for BaseClient.
|
242
|
+
def token_type
|
243
|
+
:access_token
|
244
|
+
end
|
245
|
+
|
246
|
+
# Normalizes a timestamp to a Time object.
|
247
|
+
#
|
248
|
+
# @param time [Time, String, nil] The timestamp to normalize.
|
249
|
+
#
|
250
|
+
# @return [Time, nil] The normalized Time object, or nil if the input is nil.
|
251
|
+
#
|
252
|
+
# @raise [RuntimeError] If the input is not a Time, String, or nil.
|
253
|
+
def normalize_timestamp time
|
254
|
+
case time
|
255
|
+
when NilClass
|
256
|
+
nil
|
257
|
+
when Time
|
258
|
+
time
|
259
|
+
when String
|
260
|
+
Time.parse time
|
261
|
+
else
|
262
|
+
raise "Invalid time value #{time}"
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
# Convert all keys in this hash (nested) to symbols for uniform retrieval
|
267
|
+
def recursive_hash_normalize_keys val
|
268
|
+
if val.is_a? Hash
|
269
|
+
deep_hash_normalize val
|
270
|
+
else
|
271
|
+
val
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
def deep_hash_normalize old_hash
|
276
|
+
sym_hash = {}
|
277
|
+
old_hash&.each { |k, v| sym_hash[k.to_sym] = recursive_hash_normalize_keys v }
|
278
|
+
sym_hash
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
@@ -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
|
@@ -134,10 +194,14 @@ module Google
|
|
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
|
@@ -208,8 +305,34 @@ module Google
|
|
208
305
|
assertion["scope"] = Array(@scope).join " " if @scope
|
209
306
|
assertion["aud"] = jwt_aud_uri if jwt_aud_uri
|
210
307
|
|
308
|
+
logger&.debug do
|
309
|
+
Google::Logging::Message.from message: "JWT assertion: #{assertion}"
|
310
|
+
end
|
311
|
+
|
211
312
|
JWT.encode assertion, @signing_key, SIGNING_ALGORITHM
|
212
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
|
213
336
|
end
|
214
337
|
end
|
215
338
|
end
|