googleauth 1.13.1 → 1.14.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6d8ca5b2b0c7f4ce54f7971d8de2f23f3ee0837d08d7d3c568c503308fcf82ab
4
- data.tar.gz: d5f8b8fd2fcb4fef4240db58bf90f54a8bfd021c550a7bc9063c9087285f3921
3
+ metadata.gz: 9e2eef22c1062f2fd2216760e90a1af9ece1b99838bccea486a7b4ce43be9734
4
+ data.tar.gz: 1e60ef4856de1e4f7a3d423f3953e5e39f86c7002216ad2d98ee46ec88aaaf25
5
5
  SHA512:
6
- metadata.gz: 6a4de2b23f4dc0310a18568e0618c5d81fad54dfc8d57fe3c27b954c4bd21272fcc467c2c313f98f80fa127eab11ebe0d0fc55ceed7e6c5439764500e334df49
7
- data.tar.gz: f4aff68138105ea19875bb7a51d4b6ced9ec2e7185ce725eca205ea1dc11beb9e6019c9dd89449866598ca6e4d3abb06b91f277f34e51ac7726d79a39fc40c67
6
+ metadata.gz: d5a87f962d04eb59c9ae083a4d3c46fd54ef4d4c53f0571b3952f6d1a9d30e3af89fb2c50e04b118e301097850c9985c713968dc29c62c5d388d0d8a4c3d306d
7
+ data.tar.gz: fae67434766ed2d4bb67e2c7ba2784a9397843110c28d96368d368b1dd30229a1ea83c5ee05e70a712446781e67ca84149fbaa72dd4239eb349d6943ff42b9b3
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Release History
2
2
 
3
+ ### 1.14.0 (2025-03-14)
4
+
5
+ #### Features
6
+
7
+ * add API key credentials ([#520](https://github.com/googleapis/google-auth-library-ruby/issues/520))
8
+ * Add Bearer token credentials
9
+ * add BearerToken credentials ([#522](https://github.com/googleapis/google-auth-library-ruby/issues/522))
10
+ * Update minimum Ruby version to 3.0 ([#527](https://github.com/googleapis/google-auth-library-ruby/issues/527))
11
+ #### Bug Fixes
12
+
13
+ * Eliminated the "attribute accessor as module_function" warning ([#519](https://github.com/googleapis/google-auth-library-ruby/issues/519))
14
+ * Get the project_id from gcloud ([#479](https://github.com/googleapis/google-auth-library-ruby/issues/479))
15
+ * logger configuration in service account JWT header ([#525](https://github.com/googleapis/google-auth-library-ruby/issues/525))
16
+
3
17
  ### 1.13.1 (2025-01-24)
4
18
 
5
19
  #### Bug Fixes
data/README.md CHANGED
@@ -265,7 +265,7 @@ Custom storage implementations can also be used. See
265
265
 
266
266
  ## Supported Ruby Versions
267
267
 
268
- This library is supported on Ruby 2.6+.
268
+ This library is supported on Ruby 3.0+.
269
269
 
270
270
  Google provides official support for Ruby versions that are actively supported
271
271
  by Ruby Core—that is, Ruby versions that are either in normal maintenance or
@@ -0,0 +1,155 @@
1
+ # Copyright 2025 Google LLC
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/base_client"
16
+ require "googleauth/credentials_loader"
17
+
18
+ module Google
19
+ module Auth
20
+ ##
21
+ # Implementation of Google API Key authentication.
22
+ #
23
+ # API Keys are text strings. They don't have an associated JSON file.
24
+ #
25
+ # The end-user is managing their API Keys directly, not via
26
+ # an authentication library.
27
+ #
28
+ # API Keys provide project information for an API request.
29
+ # API Keys don't reference an IAM principal, they do not expire,
30
+ # and cannot be refreshed.
31
+ #
32
+ class APIKeyCredentials
33
+ include Google::Auth::BaseClient
34
+
35
+ # @private Authorization header key
36
+ API_KEY_HEADER = "x-goog-api-key".freeze
37
+
38
+ # @private Environment variable containing API key
39
+ API_KEY_VAR = "GOOGLE_API_KEY".freeze
40
+
41
+ # @return [String] The API key
42
+ attr_reader :api_key
43
+
44
+ # @return [String] The universe domain of the universe
45
+ # this API key is for
46
+ attr_accessor :universe_domain
47
+
48
+ class << self
49
+ # Creates an APIKeyCredentials from the environment.
50
+ # Checks the ENV['GOOGLE_API_KEY'] variable.
51
+ #
52
+ # @param [String] _scope
53
+ # The scope to use for OAuth. Not used by API key auth.
54
+ # @param [Hash] options
55
+ # The options to pass to the credentials instance
56
+ #
57
+ # @return [Google::Auth::APIKeyCredentials, nil]
58
+ # Credentials if the API key environment variable is present,
59
+ # nil otherwise
60
+ def from_env _scope = nil, options = {}
61
+ api_key = ENV[API_KEY_VAR]
62
+ return nil if api_key.nil? || api_key.empty?
63
+ new options.merge(api_key: api_key)
64
+ end
65
+
66
+ # Create the APIKeyCredentials.
67
+ #
68
+ # @param [Hash] options The credentials options
69
+ # @option options [String] :api_key
70
+ # The API key to use for authentication
71
+ # @option options [String] :universe_domain
72
+ # The universe domain of the universe this API key
73
+ # belongs to (defaults to googleapis.com)
74
+ # @return [Google::Auth::APIKeyCredentials]
75
+ def make_creds options = {}
76
+ new options
77
+ end
78
+ end
79
+
80
+ # Initialize the APIKeyCredentials.
81
+ #
82
+ # @param [Hash] options The credentials options
83
+ # @option options [String] :api_key
84
+ # The API key to use for authentication
85
+ # @option options [String] :universe_domain
86
+ # The universe domain of the universe this API key
87
+ # belongs to (defaults to googleapis.com)
88
+ def initialize options = {}
89
+ raise ArgumentError, "API key must be provided" if options[:api_key].nil? || options[:api_key].empty?
90
+ @api_key = options[:api_key]
91
+ @universe_domain = options[:universe_domain] || "googleapis.com"
92
+ end
93
+
94
+ # Determines if the credentials object has expired.
95
+ # Since API keys don't expire, this always returns false.
96
+ #
97
+ # @param [Fixnum] _seconds
98
+ # The optional timeout in seconds since the last refresh
99
+ # @return [Boolean]
100
+ # True if the token has expired, false otherwise.
101
+ def expires_within? _seconds
102
+ false
103
+ end
104
+
105
+ # Creates a duplicate of these credentials.
106
+ #
107
+ # @param [Hash] options Additional options for configuring the credentials
108
+ # @return [Google::Auth::APIKeyCredentials]
109
+ def duplicate options = {}
110
+ self.class.new(
111
+ api_key: options[:api_key] || @api_key,
112
+ universe_domain: options[:universe_domain] || @universe_domain
113
+ )
114
+ end
115
+
116
+ # Updates the provided hash with the API Key header.
117
+ #
118
+ # The `apply!` method modifies the provided hash in place, adding the
119
+ # `x-goog-api-key` header with the API Key value.
120
+ #
121
+ # The API Key is hashed before being logged for security purposes.
122
+ #
123
+ # NB: this method typically would be called through `updater_proc`.
124
+ # Some older clients call it directly though, so it has to be public.
125
+ #
126
+ # @param [Hash] a_hash The hash to which the API Key header should be added.
127
+ # This is typically a hash representing the request headers. This hash
128
+ # will be modified in place.
129
+ # @param [Hash] _opts Additional options (currently not used). Included
130
+ # for consistency with the `BaseClient` interface.
131
+ # @return [Hash] The modified hash (the same hash passed as the `a_hash`
132
+ # argument).
133
+ def apply! a_hash, _opts = {}
134
+ a_hash[API_KEY_HEADER] = @api_key
135
+ logger&.debug do
136
+ hash = Digest::SHA256.hexdigest @api_key
137
+ Google::Logging::Message.from message: "Sending API key auth token. (sha256:#{hash})"
138
+ end
139
+ a_hash
140
+ end
141
+
142
+ protected
143
+
144
+ # The token type should be :api_key
145
+ def token_type
146
+ :api_key
147
+ end
148
+
149
+ # We don't need to fetch access tokens for API key auth
150
+ def fetch_access_token! _options = {}
151
+ nil
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,148 @@
1
+ # Copyright 2025 Google LLC
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/base_client"
16
+
17
+ module Google
18
+ module Auth
19
+ ##
20
+ # Implementation of Bearer Token authentication scenario.
21
+ #
22
+ # Bearer tokens are strings representing an authorization grant.
23
+ # They can be OAuth2 ("ya.29") tokens, JWTs, IDTokens -- anything
24
+ # that is sent as a `Bearer` in an `Authorization` header.
25
+ #
26
+ # Not all 'authentication' strings can be used with this class,
27
+ # e.g. an API key cannot since API keys are sent in a
28
+ # `x-goog-api-key` header or as a query parameter.
29
+ #
30
+ # This class should be used when the end-user is managing the
31
+ # authentication token separately, e.g. with a separate service.
32
+ # This means that tasks like tracking the lifetime of and
33
+ # refreshing the token are outside the scope of this class.
34
+ #
35
+ # There is no JSON representation for this type of credentials.
36
+ # If the end-user has credentials in JSON format they should typically
37
+ # use the corresponding credentials type, e.g. ServiceAccountCredentials
38
+ # with the service account JSON.
39
+ #
40
+ class BearerTokenCredentials
41
+ include Google::Auth::BaseClient
42
+
43
+ # @private Authorization header name
44
+ AUTH_METADATA_KEY = Google::Auth::BaseClient::AUTH_METADATA_KEY
45
+
46
+ # @return [String] The token to be sent as a part of Bearer claim
47
+ attr_reader :token
48
+ # The following aliasing is needed for BaseClient since it sends :token_type
49
+ alias bearer_token token
50
+
51
+ # @return [Time, nil] The token expiration time provided by the end-user.
52
+ attr_reader :expires_at
53
+
54
+ # @return [String] The universe domain of the universe
55
+ # this token is for
56
+ attr_accessor :universe_domain
57
+
58
+ class << self
59
+ # Create the BearerTokenCredentials.
60
+ #
61
+ # @param [Hash] options The credentials options
62
+ # @option options [String] :token The bearer token to use.
63
+ # @option options [Time, Numeric, nil] :expires_at The token expiration time provided by the end-user.
64
+ # Optional, for the end-user's convenience. Can be a Time object, a number of seconds since epoch.
65
+ # If `expires_at` is `nil`, it is treated as "token never expires".
66
+ # @option options [String] :universe_domain The universe domain of the universe
67
+ # this token is for (defaults to googleapis.com)
68
+ # @return [Google::Auth::BearerTokenCredentials]
69
+ def make_creds options = {}
70
+ new options
71
+ end
72
+ end
73
+
74
+ # Initialize the BearerTokenCredentials.
75
+ #
76
+ # @param [Hash] options The credentials options
77
+ # @option options [String] :token The bearer token to use.
78
+ # @option options [Time, Numeric, nil] :expires_at The token expiration time provided by the end-user.
79
+ # Optional, for the end-user's convenience. Can be a Time object, a number of seconds since epoch.
80
+ # If `expires_at` is `nil`, it is treated as "token never expires".
81
+ # @option options [String] :universe_domain The universe domain of the universe
82
+ # this token is for (defaults to googleapis.com)
83
+ def initialize options = {}
84
+ raise ArgumentError, "Bearer token must be provided" if options[:token].nil? || options[:token].empty?
85
+ @token = options[:token]
86
+ @expires_at = case options[:expires_at]
87
+ when Time
88
+ options[:expires_at]
89
+ when Numeric
90
+ Time.at options[:expires_at]
91
+ end
92
+
93
+ @universe_domain = options[:universe_domain] || "googleapis.com"
94
+ end
95
+
96
+ # Determines if the credentials object has expired.
97
+ #
98
+ # @param [Numeric] seconds The optional timeout in seconds.
99
+ # @return [Boolean] True if the token has expired, false otherwise, or
100
+ # if the expires_at was not provided.
101
+ def expires_within? seconds
102
+ return false if @expires_at.nil? # Treat nil expiration as "never expires"
103
+ Time.now + seconds >= @expires_at
104
+ end
105
+
106
+ # Creates a duplicate of these credentials.
107
+ #
108
+ # @param [Hash] options Additional options for configuring the credentials
109
+ # @option options [String] :token The bearer token to use.
110
+ # @option options [Time, Numeric] :expires_at The token expiration time. Can be a Time
111
+ # object or a number of seconds since epoch.
112
+ # @option options [String] :universe_domain The universe domain (defaults to googleapis.com)
113
+ # @return [Google::Auth::BearerTokenCredentials]
114
+ def duplicate options = {}
115
+ self.class.new(
116
+ token: options[:token] || @token,
117
+ expires_at: options[:expires_at] || @expires_at,
118
+ universe_domain: options[:universe_domain] || @universe_domain
119
+ )
120
+ end
121
+
122
+ protected
123
+
124
+ ##
125
+ # BearerTokenCredentials do not support fetching a new token.
126
+ #
127
+ # If the token has an expiration time and is expired, this method will
128
+ # raise an error.
129
+ #
130
+ # @param [Hash] _options Options for fetching a new token (not used).
131
+ # @return [nil] Always returns nil.
132
+ # @raise [StandardError] If the token is expired.
133
+ def fetch_access_token! _options = {}
134
+ if @expires_at && Time.now >= @expires_at
135
+ raise "Bearer token has expired."
136
+ end
137
+
138
+ nil
139
+ end
140
+
141
+ private
142
+
143
+ def token_type
144
+ :bearer_token
145
+ end
146
+ end
147
+ end
148
+ end
@@ -87,7 +87,7 @@ module Google
87
87
  def initialize options = {}
88
88
  # Override the constructor to remember whether the universe domain was
89
89
  # overridden by a constructor argument.
90
- @universe_domain_overridden = options["universe_domain"] || options[:universe_domain] ? true : false
90
+ @universe_domain_overridden = options["universe_domain"] || options[:universe_domain]
91
91
  # TODO: Remove when universe domain metadata endpoint is stable (see b/349488459).
92
92
  @disable_universe_domain_check = true
93
93
  super options
@@ -37,7 +37,7 @@ module Google
37
37
  AWS_SESSION_TOKEN_VAR = "AWS_SESSION_TOKEN".freeze
38
38
  GCLOUD_POSIX_COMMAND = "gcloud".freeze
39
39
  GCLOUD_WINDOWS_COMMAND = "gcloud.cmd".freeze
40
- GCLOUD_CONFIG_COMMAND = "config config-helper --format json --verbosity none".freeze
40
+ GCLOUD_CONFIG_COMMAND = "config config-helper --format json --verbosity none --quiet".freeze
41
41
 
42
42
  CREDENTIALS_FILE_NAME = "application_default_credentials.json".freeze
43
43
  NOT_FOUND_ERROR = "Unable to read the credential file specified by #{ENV_VAR}".freeze
@@ -146,7 +146,7 @@ module Google
146
146
  def load_gcloud_project_id
147
147
  gcloud = GCLOUD_WINDOWS_COMMAND if OS.windows?
148
148
  gcloud = GCLOUD_POSIX_COMMAND unless OS.windows?
149
- gcloud_json = IO.popen("#{gcloud} #{GCLOUD_CONFIG_COMMAND}", in: :close, err: :close, &:read)
149
+ gcloud_json = IO.popen("#{gcloud} #{GCLOUD_CONFIG_COMMAND}", err: :close, &:read)
150
150
  config = MultiJson.load gcloud_json
151
151
  config["configuration"]["properties"]["core"]["project"]
152
152
  rescue StandardError
@@ -16,10 +16,10 @@ require "multi_json"
16
16
  require "stringio"
17
17
 
18
18
  require "googleauth/credentials_loader"
19
+ require "googleauth/external_account"
19
20
  require "googleauth/service_account"
21
+ require "googleauth/service_account_jwt_header"
20
22
  require "googleauth/user_refresh"
21
- require "googleauth/external_account"
22
- require "googleauth/impersonated_service_account"
23
23
 
24
24
  module Google
25
25
  # Module Auth provides classes that provide Google-specific authorization
@@ -24,7 +24,13 @@ module Google
24
24
  module Connection
25
25
  module_function
26
26
 
27
- attr_accessor :default_connection
27
+ def default_connection
28
+ @default_connection
29
+ end
30
+
31
+ def default_connection= conn
32
+ @default_connection = conn
33
+ end
28
34
 
29
35
  def connection
30
36
  @default_connection || Faraday.default_connection
@@ -12,13 +12,15 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ require "jwt"
16
+ require "multi_json"
17
+ require "stringio"
18
+
15
19
  require "google/logging/message"
16
20
  require "googleauth/signet"
17
21
  require "googleauth/credentials_loader"
18
22
  require "googleauth/json_key_reader"
19
- require "jwt"
20
- require "multi_json"
21
- require "stringio"
23
+ require "googleauth/service_account_jwt_header"
22
24
 
23
25
  module Google
24
26
  # Module Auth provides classes that provide Google-specific authorization
@@ -178,161 +180,5 @@ module Google
178
180
  alt.apply! a_hash
179
181
  end
180
182
  end
181
-
182
- # Authenticates requests using Google's Service Account credentials via
183
- # JWT Header.
184
- #
185
- # This class allows authorizing requests for service accounts directly
186
- # from credentials from a json key file downloaded from the developer
187
- # console (via 'Generate new Json Key'). It is not part of any OAuth2
188
- # flow, rather it creates a JWT and sends that as a credential.
189
- #
190
- # cf [Application Default Credentials](https://cloud.google.com/docs/authentication/production)
191
- class ServiceAccountJwtHeaderCredentials
192
- JWT_AUD_URI_KEY = :jwt_aud_uri
193
- AUTH_METADATA_KEY = Google::Auth::BaseClient::AUTH_METADATA_KEY
194
- TOKEN_CRED_URI = "https://www.googleapis.com/oauth2/v4/token".freeze
195
- SIGNING_ALGORITHM = "RS256".freeze
196
- EXPIRY = 60
197
-
198
- extend CredentialsLoader
199
- extend JsonKeyReader
200
-
201
- attr_reader :project_id
202
- attr_reader :quota_project_id
203
- attr_accessor :universe_domain
204
- attr_accessor :logger
205
-
206
- # Create a ServiceAccountJwtHeaderCredentials.
207
- #
208
- # @param json_key_io [IO] an IO from which the JSON key can be read
209
- # @param scope [string|array|nil] the scope(s) to access
210
- def self.make_creds options = {}
211
- json_key_io, scope = options.values_at :json_key_io, :scope
212
- new json_key_io: json_key_io, scope: scope
213
- end
214
-
215
- # Initializes a ServiceAccountJwtHeaderCredentials.
216
- #
217
- # @param json_key_io [IO] an IO from which the JSON key can be read
218
- def initialize options = {}
219
- json_key_io = options[:json_key_io]
220
- if json_key_io
221
- @private_key, @issuer, @project_id, @quota_project_id, @universe_domain =
222
- self.class.read_json_key json_key_io
223
- else
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
229
- end
230
- @universe_domain ||= "googleapis.com"
231
- @project_id ||= CredentialsLoader.load_gcloud_project_id
232
- @signing_key = OpenSSL::PKey::RSA.new @private_key
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
261
- end
262
-
263
- # Construct a jwt token if the JWT_AUD_URI key is present in the input
264
- # hash.
265
- #
266
- # The jwt token is used as the value of a 'Bearer '.
267
- def apply! a_hash, opts = {}
268
- jwt_aud_uri = a_hash.delete JWT_AUD_URI_KEY
269
- return a_hash if jwt_aud_uri.nil? && @scope.nil?
270
- jwt_token = new_jwt_token jwt_aud_uri, opts
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
276
- a_hash
277
- end
278
-
279
- # Returns a clone of a_hash updated with the authorization header
280
- def apply a_hash, opts = {}
281
- a_copy = a_hash.clone
282
- apply! a_copy, opts
283
- a_copy
284
- end
285
-
286
- # Returns a reference to the #apply method, suitable for passing as
287
- # a closure
288
- def updater_proc
289
- proc { |a_hash, opts = {}| apply a_hash, opts }
290
- end
291
-
292
- # Creates a jwt uri token.
293
- def new_jwt_token jwt_aud_uri = nil, options = {}
294
- now = Time.new
295
- skew = options[:skew] || 60
296
- assertion = {
297
- "iss" => @issuer,
298
- "sub" => @issuer,
299
- "exp" => (now + EXPIRY).to_i,
300
- "iat" => (now - skew).to_i
301
- }
302
-
303
- jwt_aud_uri = nil if @scope
304
-
305
- assertion["scope"] = Array(@scope).join " " if @scope
306
- assertion["aud"] = jwt_aud_uri if jwt_aud_uri
307
-
308
- logger&.debug do
309
- Google::Logging::Message.from message: "JWT assertion: #{assertion}"
310
- end
311
-
312
- JWT.encode assertion, @signing_key, SIGNING_ALGORITHM
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
336
- end
337
183
  end
338
184
  end
@@ -0,0 +1,180 @@
1
+ # Copyright 2025 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 "google/logging/message"
16
+ require "googleauth/credentials_loader"
17
+ require "googleauth/json_key_reader"
18
+ require "jwt"
19
+
20
+ module Google
21
+ # Module Auth provides classes that provide Google-specific authorization
22
+ # used to access Google APIs.
23
+ module Auth
24
+ # Authenticates requests using Google's Service Account credentials via
25
+ # JWT Header.
26
+ #
27
+ # This class allows authorizing requests for service accounts directly
28
+ # from credentials from a json key file downloaded from the developer
29
+ # console (via 'Generate new Json Key'). It is not part of any OAuth2
30
+ # flow, rather it creates a JWT and sends that as a credential.
31
+ #
32
+ # cf [Application Default Credentials](https://cloud.google.com/docs/authentication/production)
33
+ class ServiceAccountJwtHeaderCredentials
34
+ JWT_AUD_URI_KEY = :jwt_aud_uri
35
+ AUTH_METADATA_KEY = Google::Auth::BaseClient::AUTH_METADATA_KEY
36
+ TOKEN_CRED_URI = "https://www.googleapis.com/oauth2/v4/token".freeze
37
+ SIGNING_ALGORITHM = "RS256".freeze
38
+ EXPIRY = 60
39
+
40
+ extend CredentialsLoader
41
+ extend JsonKeyReader
42
+
43
+ attr_reader :project_id
44
+ attr_reader :quota_project_id
45
+ attr_accessor :universe_domain
46
+ attr_accessor :logger
47
+
48
+ # Create a ServiceAccountJwtHeaderCredentials.
49
+ #
50
+ # @param json_key_io [IO] an IO from which the JSON key can be read
51
+ # @param scope [string|array|nil] the scope(s) to access
52
+ def self.make_creds options = {}
53
+ json_key_io, scope = options.values_at :json_key_io, :scope
54
+ new json_key_io: json_key_io, scope: scope
55
+ end
56
+
57
+ # Initializes a ServiceAccountJwtHeaderCredentials.
58
+ #
59
+ # @param json_key_io [IO] an IO from which the JSON key can be read
60
+ def initialize options = {}
61
+ json_key_io = options[:json_key_io]
62
+ if json_key_io
63
+ @private_key, @issuer, @project_id, @quota_project_id, @universe_domain =
64
+ self.class.read_json_key json_key_io
65
+ else
66
+ @private_key = options.key?(:private_key) ? options[:private_key] : ENV[CredentialsLoader::PRIVATE_KEY_VAR]
67
+ @issuer = options.key?(:issuer) ? options[:issuer] : ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
68
+ @project_id = options.key?(:project_id) ? options[:project_id] : ENV[CredentialsLoader::PROJECT_ID_VAR]
69
+ @quota_project_id = options[:quota_project_id] if options.key? :quota_project_id
70
+ @universe_domain = options[:universe_domain] if options.key? :universe_domain
71
+ end
72
+ @universe_domain ||= "googleapis.com"
73
+ @project_id ||= CredentialsLoader.load_gcloud_project_id
74
+ @signing_key = OpenSSL::PKey::RSA.new @private_key
75
+ @scope = options[:scope] if options.key? :scope
76
+ @logger = options[:logger] if options.key? :logger
77
+ end
78
+
79
+ # Creates a duplicate of these credentials
80
+ #
81
+ # @param options [Hash] Overrides for the credentials parameters.
82
+ # The following keys are recognized
83
+ # * `private key` the private key in string form
84
+ # * `issuer` the SA issuer
85
+ # * `scope` the scope(s) to access
86
+ # * `project_id` the project id to use during the authentication
87
+ # * `quota_project_id` the quota project id to use
88
+ # * `universe_domain` the universe domain of the credentials
89
+ def duplicate options = {}
90
+ options = deep_hash_normalize options
91
+
92
+ options = {
93
+ private_key: @private_key,
94
+ issuer: @issuer,
95
+ scope: @scope,
96
+ project_id: project_id,
97
+ quota_project_id: quota_project_id,
98
+ universe_domain: universe_domain,
99
+ logger: logger
100
+ }.merge(options)
101
+
102
+ self.class.new options
103
+ end
104
+
105
+ # Construct a jwt token if the JWT_AUD_URI key is present in the input
106
+ # hash.
107
+ #
108
+ # The jwt token is used as the value of a 'Bearer '.
109
+ def apply! a_hash, opts = {}
110
+ jwt_aud_uri = a_hash.delete JWT_AUD_URI_KEY
111
+ return a_hash if jwt_aud_uri.nil? && @scope.nil?
112
+ jwt_token = new_jwt_token jwt_aud_uri, opts
113
+ a_hash[AUTH_METADATA_KEY] = "Bearer #{jwt_token}"
114
+ logger&.debug do
115
+ hash = Digest::SHA256.hexdigest jwt_token
116
+ Google::Logging::Message.from message: "Sending JWT auth token. (sha256:#{hash})"
117
+ end
118
+ a_hash
119
+ end
120
+
121
+ # Returns a clone of a_hash updated with the authorization header
122
+ def apply a_hash, opts = {}
123
+ a_copy = a_hash.clone
124
+ apply! a_copy, opts
125
+ a_copy
126
+ end
127
+
128
+ # Returns a reference to the #apply method, suitable for passing as
129
+ # a closure
130
+ def updater_proc
131
+ proc { |a_hash, opts = {}| apply a_hash, opts }
132
+ end
133
+
134
+ # Creates a jwt uri token.
135
+ def new_jwt_token jwt_aud_uri = nil, options = {}
136
+ now = Time.new
137
+ skew = options[:skew] || 60
138
+ assertion = {
139
+ "iss" => @issuer,
140
+ "sub" => @issuer,
141
+ "exp" => (now + EXPIRY).to_i,
142
+ "iat" => (now - skew).to_i
143
+ }
144
+
145
+ jwt_aud_uri = nil if @scope
146
+
147
+ assertion["scope"] = Array(@scope).join " " if @scope
148
+ assertion["aud"] = jwt_aud_uri if jwt_aud_uri
149
+
150
+ logger&.debug do
151
+ Google::Logging::Message.from message: "JWT assertion: #{assertion}"
152
+ end
153
+
154
+ JWT.encode assertion, @signing_key, SIGNING_ALGORITHM
155
+ end
156
+
157
+ # Duck-types the corresponding method from BaseClient
158
+ def needs_access_token?
159
+ false
160
+ end
161
+
162
+ private
163
+
164
+ def deep_hash_normalize old_hash
165
+ sym_hash = {}
166
+ old_hash&.each { |k, v| sym_hash[k.to_sym] = recursive_hash_normalize_keys v }
167
+ sym_hash
168
+ end
169
+
170
+ # Convert all keys in this hash (nested) to symbols for uniform retrieval
171
+ def recursive_hash_normalize_keys val
172
+ if val.is_a? Hash
173
+ deep_hash_normalize val
174
+ else
175
+ val
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
@@ -16,6 +16,6 @@ module Google
16
16
  # Module Auth provides classes that provide Google-specific authorization
17
17
  # used to access Google APIs.
18
18
  module Auth
19
- VERSION = "1.13.1".freeze
19
+ VERSION = "1.14.0".freeze
20
20
  end
21
21
  end
data/lib/googleauth.rb CHANGED
@@ -13,9 +13,16 @@
13
13
  # limitations under the License.
14
14
 
15
15
  require "googleauth/application_default"
16
+ require "googleauth/api_key"
17
+ require "googleauth/bearer_token"
16
18
  require "googleauth/client_id"
17
19
  require "googleauth/credentials"
18
20
  require "googleauth/default_credentials"
21
+ require "googleauth/external_account"
19
22
  require "googleauth/id_tokens"
23
+ require "googleauth/impersonated_service_account"
24
+ require "googleauth/service_account"
25
+ require "googleauth/service_account_jwt_header"
20
26
  require "googleauth/user_authorizer"
27
+ require "googleauth/user_refresh"
21
28
  require "googleauth/web_user_authorizer"
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: googleauth
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.13.1
4
+ version: 1.14.0
5
5
  platform: ruby
6
6
  authors:
7
- - Tim Emiola
7
+ - Google LLC
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-01-24 00:00:00.000000000 Z
10
+ date: 2025-03-14 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: faraday
@@ -134,7 +134,7 @@ dependencies:
134
134
  description: Implements simple authorization for accessing Google APIs, and provides
135
135
  support for Application Default Credentials.
136
136
  email:
137
- - temiola@google.com
137
+ - googleapis-packages@google.com
138
138
  executables: []
139
139
  extensions: []
140
140
  extra_rdoc_files: []
@@ -146,8 +146,10 @@ files:
146
146
  - README.md
147
147
  - SECURITY.md
148
148
  - lib/googleauth.rb
149
+ - lib/googleauth/api_key.rb
149
150
  - lib/googleauth/application_default.rb
150
151
  - lib/googleauth/base_client.rb
152
+ - lib/googleauth/bearer_token.rb
151
153
  - lib/googleauth/client_id.rb
152
154
  - lib/googleauth/compute_engine.rb
153
155
  - lib/googleauth/credentials.rb
@@ -170,6 +172,7 @@ files:
170
172
  - lib/googleauth/oauth2/sts_client.rb
171
173
  - lib/googleauth/scope_util.rb
172
174
  - lib/googleauth/service_account.rb
175
+ - lib/googleauth/service_account_jwt_header.rb
173
176
  - lib/googleauth/signet.rb
174
177
  - lib/googleauth/stores/file_token_store.rb
175
178
  - lib/googleauth/stores/redis_token_store.rb
@@ -192,14 +195,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
192
195
  requirements:
193
196
  - - ">="
194
197
  - !ruby/object:Gem::Version
195
- version: '2.7'
198
+ version: '3.0'
196
199
  required_rubygems_version: !ruby/object:Gem::Requirement
197
200
  requirements:
198
201
  - - ">="
199
202
  - !ruby/object:Gem::Version
200
203
  version: '0'
201
204
  requirements: []
202
- rubygems_version: 3.6.2
205
+ rubygems_version: 3.6.5
203
206
  specification_version: 4
204
207
  summary: Google Auth Library for Ruby
205
208
  test_files: []