googleauth 1.12.1 → 1.13.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fa685deaed5ba04ae1b5740296e04ca9e8d9d0d2399e73f11e85f58e8dc51353
4
- data.tar.gz: ed5539ac8585cff94bba4589b5c8c703e063028b00af39cf10620c272ab8a199
3
+ metadata.gz: 3ebbc2ee1ff76a53cedd30312ac616884f86c2bec74838913e075f87e0fdf5a1
4
+ data.tar.gz: 845d024860649d5d9b5c25347aba13dd0f4e9335de6018b21681377e1de33127
5
5
  SHA512:
6
- metadata.gz: f2298930d07d9d6a5af75943f97bdebf69b36de8ad5bf15961cfa59d6b02466154016e91233be62e62f2abf461f8e6e7ec273852904f4d1121be36d9416c3b74
7
- data.tar.gz: 8722b0ae408e29f54e299744c875f49496322a27a065aa69ca1fd2d31f3d8f880cd2f6dcb8536551241ea91d2aa3aeb031c45b7d3768c74f035984437c918c82
6
+ metadata.gz: 11c290235e161e88e9eae0df4b82c4852cdc8d6a7d8ece59ec2ccf4ff45b374b267bcf5e5a4ef685aac400548b622ac0bbaea2ddd16f24363a5dcc3777cc21bd
7
+ data.tar.gz: 47663c4729c27848e15a427fa151da55c52645e16c84ee547ce15b7a8bfa43d0e8699a0f5d3391ebbb229dcbba1186f919fd25ffb259308e8709f8861b4b9be7
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Release History
2
2
 
3
+ ### 1.13.0 (2025-01-22)
4
+
5
+ #### Features
6
+
7
+ * create impersonated service credentials ([#499](https://github.com/googleapis/google-auth-library-ruby/issues/499))
8
+ #### Documentation
9
+
10
+ * Include note about validating externally-provided credentials ([#512](https://github.com/googleapis/google-auth-library-ruby/issues/512))
11
+
12
+ ### 1.12.2 (2024-12-19)
13
+
14
+ #### Bug Fixes
15
+
16
+ * GCECredentials lazily fetches from the metadata server to ensure a universe domain is known ([#509](https://github.com/googleapis/google-auth-library-ruby/issues/509))
17
+
3
18
  ### 1.12.1 (2024-12-17)
4
19
 
5
20
  #### Bug Fixes
data/README.md CHANGED
@@ -64,6 +64,15 @@ well as a web variant tailored toward Rack-based applications.
64
64
  The authorizers are intended for authorization use cases. For sign-on,
65
65
  see [Google Identity Platform](https://developers.google.com/identity/)
66
66
 
67
+ ## Important notes
68
+
69
+ If you accept a credential configuration (credential JSON/File/Stream) from an
70
+ external source for authentication to Google Cloud, you must validate it before
71
+ providing it to any Google API or library. Providing an unvalidated credential
72
+ configuration to Google APIs can compromise the security of your systems and data.
73
+ For more information, refer to [Validate credential configurations from external
74
+ sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
75
+
67
76
  ### Example (Web)
68
77
 
69
78
  ```ruby
@@ -93,6 +93,37 @@ module Google
93
93
  super options
94
94
  end
95
95
 
96
+ # Creates a duplicate of these credentials
97
+ # without the Signet::OAuth2::Client-specific
98
+ # transient state (e.g. cached tokens)
99
+ #
100
+ # @param options [Hash] Overrides for the credentials parameters.
101
+ # The following keys are recognized in addition to keys in the
102
+ # Signet::OAuth2::Client
103
+ # * `:universe_domain_overridden` Whether the universe domain was
104
+ # overriden during credentials creation
105
+ def duplicate options = {}
106
+ options = deep_hash_normalize options
107
+ super(
108
+ {
109
+ universe_domain_overridden: @universe_domain_overridden
110
+ }.merge(options)
111
+ )
112
+ end
113
+
114
+ # @private
115
+ # Overrides universe_domain getter to fetch lazily if it hasn't been
116
+ # fetched yet. This is necessary specifically for Compute Engine because
117
+ # the universe comes from the metadata service, and isn't known
118
+ # immediately on credential construction. All other credential types read
119
+ # the universe from their json key or other immediate input.
120
+ def universe_domain
121
+ value = super
122
+ return value unless value.nil?
123
+ fetch_access_token!
124
+ super
125
+ end
126
+
96
127
  # Overrides the super class method to change how access tokens are
97
128
  # fetched.
98
129
  def fetch_access_token _options = {}
@@ -125,6 +156,27 @@ module Google
125
156
 
126
157
  private
127
158
 
159
+ # Destructively updates these credentials.
160
+ #
161
+ # This method is called by `Signet::OAuth2::Client`'s constructor
162
+ #
163
+ # @param options [Hash] Overrides for the credentials parameters.
164
+ # The following keys are recognized in addition to keys in the
165
+ # Signet::OAuth2::Client
166
+ # * `:universe_domain_overridden` Whether the universe domain was
167
+ # overriden during credentials creation
168
+ # @return [Google::Auth::GCECredentials]
169
+ def update! options = {}
170
+ # Normalize all keys to symbols to allow indifferent access.
171
+ options = deep_hash_normalize options
172
+
173
+ @universe_domain_overridden = options[:universe_domain_overridden] if options.key? :universe_domain_overridden
174
+
175
+ super(options)
176
+
177
+ self
178
+ end
179
+
128
180
  def log_fetch_query
129
181
  if token_type == :id_token
130
182
  logger&.info do
@@ -26,6 +26,14 @@ module Google
26
26
  # In most cases, it is subclassed by API-specific credential classes that
27
27
  # can be instantiated by clients.
28
28
  #
29
+ # **Important:** If you accept a credential configuration (credential
30
+ # JSON/File/Stream) from an external source for authentication to Google
31
+ # Cloud, you must validate it before providing it to any Google API or
32
+ # library. Providing an unvalidated credential configuration to Google APIs
33
+ # can compromise the security of your systems and data. For more
34
+ # information, refer to [Validate credential configurations from external
35
+ # sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
36
+ #
29
37
  # ## Options
30
38
  #
31
39
  # Credentials classes are configured with options that dictate default
@@ -321,9 +329,6 @@ module Google
321
329
  # @return [String, Array<String>] The scope for this client. A scope is an access range
322
330
  # defined by the authorization server. The scope can be a single value or a list of values.
323
331
  #
324
- # @!attribute [r] target_audience
325
- # @return [String] The final target audience for ID tokens returned by this credential.
326
- #
327
332
  # @!attribute [r] issuer
328
333
  # @return [String] The issuer ID associated with this client.
329
334
  #
@@ -334,6 +339,9 @@ module Google
334
339
  # @return [Proc] Returns a reference to the {Signet::OAuth2::Client#apply} method,
335
340
  # suitable for passing as a closure.
336
341
  #
342
+ # @!attribute [r] target_audience
343
+ # @return [String] The final target audience for ID tokens returned by this credential.
344
+ #
337
345
  # @!attribute [rw] universe_domain
338
346
  # @return [String] The universe domain issuing these credentials.
339
347
  #
@@ -349,33 +357,53 @@ module Google
349
357
  # Creates a new Credentials instance with the provided auth credentials, and with the default
350
358
  # values configured on the class.
351
359
  #
352
- # @param [String, Hash, Signet::OAuth2::Client] keyfile
353
- # The keyfile can be provided as one of the following:
360
+ # @param [String, Hash, Signet::OAuth2::Client] source_creds
361
+ # The source of credentials. It can be provided as one of the following:
354
362
  #
355
363
  # * The path to a JSON keyfile (as a `String`)
356
364
  # * The contents of a JSON keyfile (as a `Hash`)
357
- # * A `Signet::OAuth2::Client` object
365
+ # * A `Signet::OAuth2::Client` credentials object
366
+ # * Any credentials object that supports the methods this wrapper delegates to an inner client.
367
+ #
368
+ # If this parameter is an object (`Signet::OAuth2::Client` or other) it will be used as an inner client.
369
+ # Otherwise the inner client will be constructed from the JSON keyfile or the contens of the hash.
370
+ #
358
371
  # @param [Hash] options
359
- # The options for configuring the credentials instance. The following is supported:
372
+ # The options for configuring this wrapper credentials object and the inner client.
373
+ # The options hash is used in two ways:
360
374
  #
361
- # * `:scope` - the scope for the client
362
- # * `project_id` (and optionally `project`) - the project identifier for the client
363
- # * `:connection_builder` - the connection builder to use for the client
364
- # * `:default_connection` - the default connection to use for the client
365
- # * `:logger` - the logger used to log credential operations such as token refresh.
375
+ # 1. **Configuring the wrapper object:** Some options are used to directly
376
+ # configure the wrapper `Credentials` instance. These include:
366
377
  #
367
- def initialize keyfile, options = {}
368
- verify_keyfile_provided! keyfile
378
+ # * `:project_id` (and optionally `:project`) - the project identifier for the client
379
+ # * `:quota_project_id` - the quota project identifier for the client
380
+ # * `:logger` - the logger used to log credential operations such as token refresh.
381
+ #
382
+ # 2. **Configuring the inner client:** When the `source_creds` parameter
383
+ # is a `String` or `Hash`, a new `Signet::OAuth2::Client` is created
384
+ # internally. The following options are used to configure this inner client:
385
+ #
386
+ # * `:scope` - the scope for the client
387
+ # * `:target_audience` - the target audience for the client
388
+ #
389
+ # Any other options in the `options` hash are passed directly to the
390
+ # inner client constructor. This allows you to configure additional
391
+ # parameters of the `Signet::OAuth2::Client`, such as connection parameters,
392
+ # timeouts, etc.
393
+ #
394
+ def initialize source_creds, options = {}
395
+ raise "The source credentials passed to Google::Auth::Credentials.new were nil." if source_creds.nil?
396
+
369
397
  options = symbolize_hash_keys options
370
398
  @project_id = options[:project_id] || options[:project]
371
399
  @quota_project_id = options[:quota_project_id]
372
- case keyfile
373
- when Google::Auth::BaseClient
374
- update_from_signet keyfile
400
+ case source_creds
401
+ when String
402
+ update_from_filepath source_creds, options
375
403
  when Hash
376
- update_from_hash keyfile, options
404
+ update_from_hash source_creds, options
377
405
  else
378
- update_from_filepath keyfile, options
406
+ update_from_client source_creds
379
407
  end
380
408
  setup_logging logger: options.fetch(:logger, :default)
381
409
  @project_id ||= CredentialsLoader.load_gcloud_project_id
@@ -481,14 +509,50 @@ module Google
481
509
  :from_application_default,
482
510
  :from_io
483
511
 
484
- protected
485
512
 
486
- # Verify that the keyfile argument is provided.
487
- def verify_keyfile_provided! keyfile
488
- return unless keyfile.nil?
489
- raise "The keyfile passed to Google::Auth::Credentials.new was nil."
513
+ # Creates a duplicate of these credentials. This method tries to create the duplicate of the
514
+ # wrapped credentials if they support duplication and use them as is if they don't.
515
+ #
516
+ # The wrapped credentials are typically `Signet::OAuth2::Client` objects and they keep
517
+ # the transient state (token, refresh token, etc). The duplication discards that state,
518
+ # allowing e.g. to get the token with a different scope.
519
+ #
520
+ # @param options [Hash] Overrides for the credentials parameters.
521
+ #
522
+ # The options hash is used in two ways:
523
+ #
524
+ # 1. **Configuring the duplicate of the wrapper object:** Some options are used to directly
525
+ # configure the wrapper `Credentials` instance. These include:
526
+ #
527
+ # * `:project_id` (and optionally `:project`) - the project identifier for the credentials
528
+ # * `:quota_project_id` - the quota project identifier for the credentials
529
+ #
530
+ # 2. **Configuring the duplicate of the inner client:** If the inner client supports duplication
531
+ # the options hash is passed to it. This allows for configuration of additional parameters,
532
+ # most importantly (but not limited to) the following:
533
+ #
534
+ # * `:scope` - the scope for the client
535
+ #
536
+ # @return [Credentials]
537
+ def duplicate options = {}
538
+ options = deep_hash_normalize options
539
+
540
+ options = {
541
+ project_id: @project_id,
542
+ quota_project_id: @quota_project_id
543
+ }.merge(options)
544
+
545
+ new_client = if @client.respond_to? :duplicate
546
+ @client.duplicate options
547
+ else
548
+ @client
549
+ end
550
+
551
+ self.class.new new_client, options
490
552
  end
491
553
 
554
+ protected
555
+
492
556
  # Verify that the keyfile argument is a file.
493
557
  def verify_keyfile_exists! keyfile
494
558
  exists = ::File.file? keyfile
@@ -530,11 +594,12 @@ module Google
530
594
  options
531
595
  end
532
596
 
533
- def update_from_signet client
597
+ def update_from_client client
534
598
  @project_id ||= client.project_id if client.respond_to? :project_id
535
599
  @quota_project_id ||= client.quota_project_id if client.respond_to? :quota_project_id
536
600
  @client = client
537
601
  end
602
+ alias update_from_signet update_from_client
538
603
 
539
604
  def update_from_hash hash, options
540
605
  hash = stringify_hash_keys hash
@@ -571,6 +636,23 @@ module Google
571
636
  end
572
637
  @client.logger = logger
573
638
  end
639
+
640
+ private
641
+
642
+ # Convert all keys in this hash (nested) to symbols for uniform retrieval
643
+ def recursive_hash_normalize_keys val
644
+ if val.is_a? Hash
645
+ deep_hash_normalize val
646
+ else
647
+ val
648
+ end
649
+ end
650
+
651
+ def deep_hash_normalize old_hash
652
+ sym_hash = {}
653
+ old_hash&.each { |k, v| sym_hash[k.to_sym] = recursive_hash_normalize_keys v }
654
+ sym_hash
655
+ end
574
656
  end
575
657
  end
576
658
  end
@@ -19,6 +19,7 @@ require "googleauth/credentials_loader"
19
19
  require "googleauth/service_account"
20
20
  require "googleauth/user_refresh"
21
21
  require "googleauth/external_account"
22
+ require "googleauth/impersonated_service_account"
22
23
 
23
24
  module Google
24
25
  # Module Auth provides classes that provide Google-specific authorization
@@ -29,8 +30,18 @@ module Google
29
30
  class DefaultCredentials
30
31
  extend CredentialsLoader
31
32
 
32
- # override CredentialsLoader#make_creds to use the class determined by
33
+ ##
34
+ # Override CredentialsLoader#make_creds to use the class determined by
33
35
  # loading the json.
36
+ #
37
+ # **Important:** If you accept a credential configuration (credential
38
+ # JSON/File/Stream) from an external source for authentication to Google
39
+ # Cloud, you must validate it before providing it to any Google API or
40
+ # library. Providing an unvalidated credential configuration to Google
41
+ # APIs can compromise the security of your systems and data. For more
42
+ # information, refer to [Validate credential configurations from external
43
+ # sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
44
+ #
34
45
  def self.make_creds options = {}
35
46
  json_key_io = options[:json_key_io]
36
47
  if json_key_io
@@ -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
@@ -81,6 +81,30 @@ module Google
81
81
  .configure_connection(options)
82
82
  end
83
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
+
84
108
  # Handles certain escape sequences that sometimes appear in input.
85
109
  # Specifically, interprets the "\n" sequence for newline, and removes
86
110
  # enclosing quotes.
@@ -114,6 +138,32 @@ module Google
114
138
 
115
139
  private
116
140
 
141
+ # Destructively updates these credentials
142
+ #
143
+ # This method is called by `Signet::OAuth2::Client`'s constructor
144
+ #
145
+ # @param options [Hash] Overrides for the credentials parameters.
146
+ # The following keys are recognized in addition to keys in the
147
+ # Signet::OAuth2::Client
148
+ # * `:enable_self_signed_jwt` Whether the self-signed JWT should
149
+ # be used for the authentication
150
+ # * `project_id` the project id to use during the authentication
151
+ # * `quota_project_id` the quota project id to use
152
+ # during the authentication
153
+ # @return [Google::Auth::ServiceAccountCredentials]
154
+ def update! options = {}
155
+ # Normalize all keys to symbols to allow indifferent access.
156
+ options = deep_hash_normalize options
157
+
158
+ @enable_self_signed_jwt = options[:enable_self_signed_jwt] ? true : false
159
+ @project_id = options[:project_id] if options.key? :project_id
160
+ @quota_project_id = options[:quota_project_id] if options.key? :quota_project_id
161
+
162
+ super(options)
163
+
164
+ self
165
+ end
166
+
117
167
  def apply_self_signed_jwt! a_hash
118
168
  # Use the ServiceAccountJwtHeaderCredentials using the same cred values
119
169
  cred_json = {
@@ -144,8 +194,10 @@ module Google
144
194
  TOKEN_CRED_URI = "https://www.googleapis.com/oauth2/v4/token".freeze
145
195
  SIGNING_ALGORITHM = "RS256".freeze
146
196
  EXPIRY = 60
197
+
147
198
  extend CredentialsLoader
148
199
  extend JsonKeyReader
200
+
149
201
  attr_reader :project_id
150
202
  attr_reader :quota_project_id
151
203
  attr_accessor :universe_domain
@@ -169,16 +221,43 @@ module Google
169
221
  @private_key, @issuer, @project_id, @quota_project_id, @universe_domain =
170
222
  self.class.read_json_key json_key_io
171
223
  else
172
- @private_key = ENV[CredentialsLoader::PRIVATE_KEY_VAR]
173
- @issuer = ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
174
- @project_id = ENV[CredentialsLoader::PROJECT_ID_VAR]
175
- @quota_project_id = nil
176
- @universe_domain = nil
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
177
229
  end
178
230
  @universe_domain ||= "googleapis.com"
179
231
  @project_id ||= CredentialsLoader.load_gcloud_project_id
180
232
  @signing_key = OpenSSL::PKey::RSA.new @private_key
181
- @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
182
261
  end
183
262
 
184
263
  # Construct a jwt token if the JWT_AUD_URI key is present in the input
@@ -237,6 +316,23 @@ module Google
237
316
  def needs_access_token?
238
317
  false
239
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
240
336
  end
241
337
  end
242
338
  end
@@ -38,6 +38,22 @@ module Signet
38
38
  self
39
39
  end
40
40
 
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
45
+
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
53
+
54
+ update_signet_base options
55
+ end
56
+
41
57
  def configure_connection options
42
58
  @connection_info =
43
59
  options[:connection_builder] || options[:default_connection]
@@ -117,6 +133,42 @@ module Signet
117
133
  end
118
134
  end
119
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
+
120
172
  private
121
173
 
122
174
  def expires_at_from_id_token id_token
@@ -85,6 +85,26 @@ module Google
85
85
  super options
86
86
  end
87
87
 
88
+ # Creates a duplicate of these credentials
89
+ # without the Signet::OAuth2::Client-specific
90
+ # transient state (e.g. cached tokens)
91
+ #
92
+ # @param options [Hash] Overrides for the credentials parameters.
93
+ # The following keys are recognized in addition to keys in the
94
+ # Signet::OAuth2::Client
95
+ # * `project_id` the project id to use during the authentication
96
+ # * `quota_project_id` the quota project id to use
97
+ # during the authentication
98
+ def duplicate options = {}
99
+ options = deep_hash_normalize options
100
+ super(
101
+ {
102
+ project_id: @project_id,
103
+ quota_project_id: @quota_project_id
104
+ }.merge(options)
105
+ )
106
+ end
107
+
88
108
  # Revokes the credential
89
109
  def revoke! options = {}
90
110
  c = options[:connection] || Faraday.default_connection
@@ -114,6 +134,31 @@ module Google
114
134
  Google::Auth::ScopeUtil.normalize(scope)
115
135
  missing_scope.empty?
116
136
  end
137
+
138
+ private
139
+
140
+ # Destructively updates these credentials
141
+ #
142
+ # This method is called by `Signet::OAuth2::Client`'s constructor
143
+ #
144
+ # @param options [Hash] Overrides for the credentials parameters.
145
+ # The following keys are recognized in addition to keys in the
146
+ # Signet::OAuth2::Client
147
+ # * `project_id` the project id to use during the authentication
148
+ # * `quota_project_id` the quota project id to use
149
+ # during the authentication
150
+ # @return [Google::Auth::UserRefreshCredentials]
151
+ def update! options = {}
152
+ # Normalize all keys to symbols to allow indifferent access.
153
+ options = deep_hash_normalize options
154
+
155
+ @project_id = options[:project_id] if options.key? :project_id
156
+ @quota_project_id = options[:quota_project_id] if options.key? :quota_project_id
157
+
158
+ super(options)
159
+
160
+ self
161
+ end
117
162
  end
118
163
  end
119
164
  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.12.1".freeze
19
+ VERSION = "1.13.0".freeze
20
20
  end
21
21
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: googleauth
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.12.1
4
+ version: 1.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tim Emiola
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-12-18 00:00:00.000000000 Z
10
+ date: 2025-01-22 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: faraday
@@ -166,6 +165,7 @@ files:
166
165
  - lib/googleauth/id_tokens/errors.rb
167
166
  - lib/googleauth/id_tokens/key_sources.rb
168
167
  - lib/googleauth/id_tokens/verifier.rb
168
+ - lib/googleauth/impersonated_service_account.rb
169
169
  - lib/googleauth/json_key_reader.rb
170
170
  - lib/googleauth/oauth2/sts_client.rb
171
171
  - lib/googleauth/scope_util.rb
@@ -185,7 +185,6 @@ metadata:
185
185
  changelog_uri: https://github.com/googleapis/google-auth-library-ruby/blob/main/CHANGELOG.md
186
186
  source_code_uri: https://github.com/googleapis/google-auth-library-ruby
187
187
  bug_tracker_uri: https://github.com/googleapis/google-auth-library-ruby/issues
188
- post_install_message:
189
188
  rdoc_options: []
190
189
  require_paths:
191
190
  - lib
@@ -200,8 +199,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
200
199
  - !ruby/object:Gem::Version
201
200
  version: '0'
202
201
  requirements: []
203
- rubygems_version: 3.5.23
204
- signing_key:
202
+ rubygems_version: 3.6.2
205
203
  specification_version: 4
206
204
  summary: Google Auth Library for Ruby
207
205
  test_files: []