googleauth 1.12.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 34f510693238aa2d0ac63fb3d7a91f8685d34dc9962e33952220b6ee8845beef
4
- data.tar.gz: 97d0eb5127ac1c609740d8b422b2002bd8f983043ff1a11cc09130c3c650d30f
3
+ metadata.gz: 6d8ca5b2b0c7f4ce54f7971d8de2f23f3ee0837d08d7d3c568c503308fcf82ab
4
+ data.tar.gz: d5f8b8fd2fcb4fef4240db58bf90f54a8bfd021c550a7bc9063c9087285f3921
5
5
  SHA512:
6
- metadata.gz: 821238707fdf60880359514bc2385a273b4d16fe6bc6bd8ad804fcd36d2255667ad4a4dd39e7fabbd5f422367208a7fc7b74949943cda444481a8da3edfd16e2
7
- data.tar.gz: 792636b6a808d5c93dc7a3883a7c7d1e62cbb3d8b33b76e4da025cdbdac8cbd915c1cd4c5e1e698a58173d6fb2840e7623bd8c2a5673edc69113b662486029c4
6
+ metadata.gz: 6a4de2b23f4dc0310a18568e0618c5d81fad54dfc8d57fe3c27b954c4bd21272fcc467c2c313f98f80fa127eab11ebe0d0fc55ceed7e6c5439764500e334df49
7
+ data.tar.gz: f4aff68138105ea19875bb7a51d4b6ced9ec2e7185ce725eca205ea1dc11beb9e6019c9dd89449866598ca6e4d3abb06b91f277f34e51ac7726d79a39fc40c67
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Release History
2
2
 
3
+ ### 1.13.1 (2025-01-24)
4
+
5
+ #### Bug Fixes
6
+
7
+ * Signet client subclasses no longer make the update! method private ([#516](https://github.com/googleapis/google-auth-library-ruby/issues/516))
8
+
9
+ ### 1.13.0 (2025-01-22)
10
+
11
+ #### Features
12
+
13
+ * create impersonated service credentials ([#499](https://github.com/googleapis/google-auth-library-ruby/issues/499))
14
+ #### Documentation
15
+
16
+ * Include note about validating externally-provided credentials ([#512](https://github.com/googleapis/google-auth-library-ruby/issues/512))
17
+
3
18
  ### 1.12.2 (2024-12-19)
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,24 @@ 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
+
96
114
  # @private
97
115
  # Overrides universe_domain getter to fetch lazily if it hasn't been
98
116
  # fetched yet. This is necessary specifically for Compute Engine because
@@ -136,6 +154,27 @@ module Google
136
154
  end
137
155
  end
138
156
 
157
+ # Destructively updates these credentials.
158
+ #
159
+ # This method is called by `Signet::OAuth2::Client`'s constructor
160
+ #
161
+ # @param options [Hash] Overrides for the credentials parameters.
162
+ # The following keys are recognized in addition to keys in the
163
+ # Signet::OAuth2::Client
164
+ # * `:universe_domain_overridden` Whether the universe domain was
165
+ # overriden during credentials creation
166
+ # @return [Google::Auth::GCECredentials]
167
+ def update! options = {}
168
+ # Normalize all keys to symbols to allow indifferent access.
169
+ options = deep_hash_normalize options
170
+
171
+ @universe_domain_overridden = options[:universe_domain_overridden] if options.key? :universe_domain_overridden
172
+
173
+ super(options)
174
+
175
+ self
176
+ end
177
+
139
178
  private
140
179
 
141
180
  def log_fetch_query
@@ -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.
@@ -112,6 +136,32 @@ module Google
112
136
  super && !enable_self_signed_jwt?
113
137
  end
114
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
+
115
165
  private
116
166
 
117
167
  def apply_self_signed_jwt! a_hash
@@ -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,29 @@ module Google
114
134
  Google::Auth::ScopeUtil.normalize(scope)
115
135
  missing_scope.empty?
116
136
  end
137
+
138
+ # Destructively updates these credentials
139
+ #
140
+ # This method is called by `Signet::OAuth2::Client`'s constructor
141
+ #
142
+ # @param options [Hash] Overrides for the credentials parameters.
143
+ # The following keys are recognized in addition to keys in the
144
+ # Signet::OAuth2::Client
145
+ # * `project_id` the project id to use during the authentication
146
+ # * `quota_project_id` the quota project id to use
147
+ # during the authentication
148
+ # @return [Google::Auth::UserRefreshCredentials]
149
+ def update! options = {}
150
+ # Normalize all keys to symbols to allow indifferent access.
151
+ options = deep_hash_normalize options
152
+
153
+ @project_id = options[:project_id] if options.key? :project_id
154
+ @quota_project_id = options[:quota_project_id] if options.key? :quota_project_id
155
+
156
+ super(options)
157
+
158
+ self
159
+ end
117
160
  end
118
161
  end
119
162
  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.2".freeze
19
+ VERSION = "1.13.1".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.2
4
+ version: 1.13.1
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-19 00:00:00.000000000 Z
10
+ date: 2025-01-24 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: []