googleauth 1.8.0 → 1.15.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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +117 -0
  3. data/Credentials.md +106 -0
  4. data/Errors.md +152 -0
  5. data/README.md +49 -1
  6. data/lib/googleauth/api_key.rb +164 -0
  7. data/lib/googleauth/application_default.rb +6 -8
  8. data/lib/googleauth/base_client.rb +21 -4
  9. data/lib/googleauth/bearer_token.rb +162 -0
  10. data/lib/googleauth/client_id.rb +9 -6
  11. data/lib/googleauth/compute_engine.rb +231 -49
  12. data/lib/googleauth/credentials.rb +187 -58
  13. data/lib/googleauth/credentials_loader.rb +11 -20
  14. data/lib/googleauth/default_credentials.rb +29 -8
  15. data/lib/googleauth/errors.rb +117 -0
  16. data/lib/googleauth/external_account/aws_credentials.rb +85 -18
  17. data/lib/googleauth/external_account/base_credentials.rb +67 -6
  18. data/lib/googleauth/external_account/external_account_utils.rb +15 -4
  19. data/lib/googleauth/external_account/identity_pool_credentials.rb +40 -15
  20. data/lib/googleauth/external_account/pluggable_credentials.rb +34 -19
  21. data/lib/googleauth/external_account.rb +32 -7
  22. data/lib/googleauth/helpers/connection.rb +7 -1
  23. data/lib/googleauth/iam.rb +19 -3
  24. data/lib/googleauth/id_tokens/errors.rb +13 -7
  25. data/lib/googleauth/id_tokens/key_sources.rb +13 -7
  26. data/lib/googleauth/id_tokens/verifier.rb +2 -3
  27. data/lib/googleauth/id_tokens.rb +4 -6
  28. data/lib/googleauth/impersonated_service_account.rb +329 -0
  29. data/lib/googleauth/json_key_reader.rb +13 -3
  30. data/lib/googleauth/oauth2/sts_client.rb +9 -4
  31. data/lib/googleauth/scope_util.rb +1 -1
  32. data/lib/googleauth/service_account.rb +84 -104
  33. data/lib/googleauth/service_account_jwt_header.rb +187 -0
  34. data/lib/googleauth/signet.rb +169 -4
  35. data/lib/googleauth/token_store.rb +3 -3
  36. data/lib/googleauth/user_authorizer.rb +89 -11
  37. data/lib/googleauth/user_refresh.rb +72 -9
  38. data/lib/googleauth/version.rb +1 -1
  39. data/lib/googleauth/web_user_authorizer.rb +65 -17
  40. data/lib/googleauth.rb +8 -0
  41. metadata +45 -13
@@ -15,6 +15,7 @@
15
15
  require "time"
16
16
  require "uri"
17
17
  require "googleauth/credentials_loader"
18
+ require "googleauth/errors"
18
19
  require "googleauth/external_account/aws_credentials"
19
20
  require "googleauth/external_account/identity_pool_credentials"
20
21
  require "googleauth/external_account/pluggable_credentials"
@@ -35,30 +36,41 @@ module Google
35
36
 
36
37
  # Create a ExternalAccount::Credentials
37
38
  #
38
- # @param json_key_io [IO] an IO from which the JSON key can be read
39
- # @param scope [String,Array,nil] the scope(s) to access
39
+ # @param options [Hash] Options for creating credentials
40
+ # @option options [IO] :json_key_io (required) An IO object containing the JSON key
41
+ # @option options [String,Array,nil] :scope The scope(s) to access
42
+ # @return [Google::Auth::ExternalAccount::AwsCredentials,
43
+ # Google::Auth::ExternalAccount::IdentityPoolCredentials,
44
+ # Google::Auth::ExternalAccount::PluggableAuthCredentials]
45
+ # The appropriate external account credentials based on the credential source
46
+ # @raise [Google::Auth::InitializationError] If the json file is missing, lacks required fields,
47
+ # or does not contain a supported credential source
40
48
  def self.make_creds options = {}
41
49
  json_key_io, scope = options.values_at :json_key_io, :scope
42
50
 
43
- raise "A json file is required for external account credentials." unless json_key_io
51
+ raise InitializationError, "A json file is required for external account credentials." unless json_key_io
44
52
  user_creds = read_json_key json_key_io
45
53
 
46
54
  # AWS credentials is determined by aws subject token type
47
55
  return make_aws_credentials user_creds, scope if user_creds[:subject_token_type] == AWS_SUBJECT_TOKEN_TYPE
48
56
 
49
- raise MISSING_CREDENTIAL_SOURCE if user_creds[:credential_source].nil?
57
+ raise InitializationError, MISSING_CREDENTIAL_SOURCE if user_creds[:credential_source].nil?
50
58
  user_creds[:scope] = scope
51
59
  make_external_account_credentials user_creds
52
60
  end
53
61
 
54
62
  # Reads the required fields from the JSON.
63
+ #
64
+ # @param json_key_io [IO] An IO object containing the JSON key
65
+ # @return [Hash] The parsed JSON key
66
+ # @raise [Google::Auth::InitializationError] If the JSON is missing required fields
55
67
  def self.read_json_key json_key_io
56
68
  json_key = MultiJson.load json_key_io.read, symbolize_keys: true
57
69
  wanted = [
58
70
  :audience, :subject_token_type, :token_url, :credential_source
59
71
  ]
60
72
  wanted.each do |key|
61
- raise "the json is missing the #{key} field" unless json_key.key? key
73
+ raise InitializationError, "the json is missing the #{key} field" unless json_key.key? key
62
74
  end
63
75
  json_key
64
76
  end
@@ -66,6 +78,11 @@ module Google
66
78
  class << self
67
79
  private
68
80
 
81
+ # Creates AWS credentials from the provided user credentials
82
+ #
83
+ # @param user_creds [Hash] The user credentials containing AWS credential source information
84
+ # @param scope [String,Array,nil] The scope(s) to access
85
+ # @return [Google::Auth::ExternalAccount::AwsCredentials] The AWS credentials
69
86
  def make_aws_credentials user_creds, scope
70
87
  Google::Auth::ExternalAccount::AwsCredentials.new(
71
88
  audience: user_creds[:audience],
@@ -73,10 +90,18 @@ module Google
73
90
  subject_token_type: user_creds[:subject_token_type],
74
91
  token_url: user_creds[:token_url],
75
92
  credential_source: user_creds[:credential_source],
76
- service_account_impersonation_url: user_creds[:service_account_impersonation_url]
93
+ service_account_impersonation_url: user_creds[:service_account_impersonation_url],
94
+ universe_domain: user_creds[:universe_domain]
77
95
  )
78
96
  end
79
97
 
98
+ # Creates the appropriate external account credentials based on the credential source type
99
+ #
100
+ # @param user_creds [Hash] The user credentials containing credential source information
101
+ # @return [Google::Auth::ExternalAccount::IdentityPoolCredentials,
102
+ # Google::Auth::ExternalAccount::PluggableAuthCredentials]
103
+ # The appropriate external account credentials
104
+ # @raise [Google::Auth::InitializationError] If the credential source is not a supported type
80
105
  def make_external_account_credentials user_creds
81
106
  unless user_creds[:credential_source][:file].nil? && user_creds[:credential_source][:url].nil?
82
107
  return Google::Auth::ExternalAccount::IdentityPoolCredentials.new user_creds
@@ -84,7 +109,7 @@ module Google
84
109
  unless user_creds[:credential_source][:executable].nil?
85
110
  return Google::Auth::ExternalAccount::PluggableAuthCredentials.new user_creds
86
111
  end
87
- raise INVALID_EXTERNAL_ACCOUNT_TYPE
112
+ raise InitializationError, INVALID_EXTERNAL_ACCOUNT_TYPE
88
113
  end
89
114
  end
90
115
  end
@@ -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
@@ -27,8 +27,9 @@ module Google
27
27
 
28
28
  # Initializes an IAMCredentials.
29
29
  #
30
- # @param selector the IAM selector.
31
- # @param token the IAM token.
30
+ # @param selector [String] The IAM selector.
31
+ # @param token [String] The IAM token.
32
+ # @raise [TypeError] If selector or token is not a String
32
33
  def initialize selector, token
33
34
  raise TypeError unless selector.is_a? String
34
35
  raise TypeError unless token.is_a? String
@@ -37,13 +38,19 @@ module Google
37
38
  end
38
39
 
39
40
  # Adds the credential fields to the hash.
41
+ #
42
+ # @param a_hash [Hash] The hash to update with credentials
43
+ # @return [Hash] The updated hash with credentials
40
44
  def apply! a_hash
41
45
  a_hash[SELECTOR_KEY] = @selector
42
46
  a_hash[TOKEN_KEY] = @token
43
47
  a_hash
44
48
  end
45
49
 
46
- # Returns a clone of a_hash updated with the authoriation header
50
+ # Returns a clone of a_hash updated with the authorization header
51
+ #
52
+ # @param a_hash [Hash] The hash to clone and update with credentials
53
+ # @return [Hash] A new hash with credentials
47
54
  def apply a_hash
48
55
  a_copy = a_hash.clone
49
56
  apply! a_copy
@@ -52,9 +59,18 @@ module Google
52
59
 
53
60
  # Returns a reference to the #apply method, suitable for passing as
54
61
  # a closure
62
+ #
63
+ # @return [Proc] A procedure that updates a hash with credentials
55
64
  def updater_proc
56
65
  proc { |a_hash, _opts = {}| apply a_hash }
57
66
  end
67
+
68
+ # Returns the IAM authority selector as the principal
69
+ # @private
70
+ # @return [String] the IAM authoirty selector
71
+ def principal
72
+ @selector
73
+ end
58
74
  end
59
75
  end
60
76
  end
@@ -14,6 +14,8 @@
14
14
  # See the License for the specific language governing permissions and
15
15
  # limitations under the License.
16
16
 
17
+ require "googleauth/errors"
18
+
17
19
 
18
20
  module Google
19
21
  module Auth
@@ -21,35 +23,39 @@ module Google
21
23
  ##
22
24
  # Failed to obtain keys from the key source.
23
25
  #
24
- class KeySourceError < StandardError; end
26
+ class KeySourceError < StandardError
27
+ include Google::Auth::Error
28
+ end
25
29
 
26
30
  ##
27
31
  # Failed to verify a token.
28
32
  #
29
- class VerificationError < StandardError; end
33
+ class VerificationError < StandardError
34
+ include Google::Auth::Error
35
+ end
30
36
 
31
37
  ##
32
- # Failed to verify a token because it is expired.
38
+ # Failed to verify token because it is expired.
33
39
  #
34
40
  class ExpiredTokenError < VerificationError; end
35
41
 
36
42
  ##
37
- # Failed to verify a token because its signature did not match.
43
+ # Failed to verify token because its signature did not match.
38
44
  #
39
45
  class SignatureError < VerificationError; end
40
46
 
41
47
  ##
42
- # Failed to verify a token because its issuer did not match.
48
+ # Failed to verify token because its issuer did not match.
43
49
  #
44
50
  class IssuerMismatchError < VerificationError; end
45
51
 
46
52
  ##
47
- # Failed to verify a token because its audience did not match.
53
+ # Failed to verify token because its audience did not match.
48
54
  #
49
55
  class AudienceMismatchError < VerificationError; end
50
56
 
51
57
  ##
52
- # Failed to verify a token because its authorized party did not match.
58
+ # Failed to verify token because its authorized party did not match.
53
59
  #
54
60
  class AuthorizedPartyMismatchError < VerificationError; end
55
61
  end
@@ -72,8 +72,8 @@ module Google
72
72
  #
73
73
  # @param jwk [Hash,String] The JWK specification.
74
74
  # @return [KeyInfo]
75
- # @raise [KeySourceError] If the key could not be extracted from the
76
- # JWK.
75
+ # @raise [Google::Auth::IDTokens::KeySourceError] If the key could not be extracted from the
76
+ # JWK due to invalid type, malformed JSON, or invalid key data.
77
77
  #
78
78
  def from_jwk jwk
79
79
  jwk = symbolize_keys ensure_json_parsed jwk
@@ -94,10 +94,10 @@ module Google
94
94
  # Create an array of KeyInfo from a JWK Set, which may be given as
95
95
  # either a hash or an unparsed JSON string.
96
96
  #
97
- # @param jwk [Hash,String] The JWK Set specification.
97
+ # @param jwk_set [Hash,String] The JWK Set specification.
98
98
  # @return [Array<KeyInfo>]
99
- # @raise [KeySourceError] If a key could not be extracted from the
100
- # JWK Set.
99
+ # @raise [Google::Auth::IDTokens::KeySourceError] If a key could not be extracted from the
100
+ # JWK Set, or if the set contains no keys.
101
101
  #
102
102
  def from_jwk_set jwk_set
103
103
  jwk_set = symbolize_keys ensure_json_parsed jwk_set
@@ -261,7 +261,8 @@ module Google
261
261
  # return the new keys.
262
262
  #
263
263
  # @return [Array<KeyInfo>]
264
- # @raise [KeySourceError] if key retrieval failed.
264
+ # @raise [Google::Auth::IDTokens::KeySourceError] If key retrieval fails, JSON parsing
265
+ # fails, or the data cannot be interpreted as keys
265
266
  #
266
267
  def refresh_keys
267
268
  @monitor.synchronize do
@@ -310,6 +311,11 @@ module Google
310
311
 
311
312
  protected
312
313
 
314
+ # Interpret JSON data as X509 certificates
315
+ #
316
+ # @param data [Hash] The JSON data containing certificate strings
317
+ # @return [Array<KeyInfo>] Array of key info objects
318
+ # @raise [Google::Auth::IDTokens::KeySourceError] If X509 certificates cannot be parsed
313
319
  def interpret_json data
314
320
  data.map do |id, cert_str|
315
321
  key = OpenSSL::X509::Certificate.new(cert_str).public_key
@@ -371,7 +377,7 @@ module Google
371
377
  # Attempt to refresh keys and return the new keys.
372
378
  #
373
379
  # @return [Array<KeyInfo>]
374
- # @raise [KeySourceError] if key retrieval failed.
380
+ # @raise [Google::Auth::IDTokens::KeySourceError] If key retrieval failed for any source.
375
381
  #
376
382
  def refresh_keys
377
383
  @sources.flat_map(&:refresh_keys)
@@ -61,10 +61,9 @@ module Google
61
61
  # @param iss [String,nil] If given, override the `iss` check.
62
62
  #
63
63
  # @return [Hash] the decoded payload, if verification succeeded.
64
- # @raise [KeySourceError] if the key source failed to obtain public keys
65
- # @raise [VerificationError] if the token verification failed.
64
+ # @raise [Google::Auth::IDTokens::KeySourceError] if the key source failed to obtain public keys
65
+ # @raise [Google::Auth::IDTokens::VerificationError] if the token verification failed.
66
66
  # Additional data may be available in the error subclass and message.
67
- #
68
67
  def verify token,
69
68
  key_source: :default,
70
69
  aud: :default,
@@ -160,15 +160,14 @@ module Google
160
160
  # checking is performed. Default is to check against {OIDC_ISSUERS}.
161
161
  #
162
162
  # @return [Hash] The decoded token payload.
163
- # @raise [KeySourceError] if the key source failed to obtain public keys
164
- # @raise [VerificationError] if the token verification failed.
163
+ # @raise [Google::Auth::IDTokens::KeySourceError] if the key source failed to obtain public keys
164
+ # @raise [Google::Auth::IDTokens::VerificationError] if the token verification failed.
165
165
  # Additional data may be available in the error subclass and message.
166
166
  #
167
167
  def verify_oidc token,
168
168
  aud: nil,
169
169
  azp: nil,
170
170
  iss: OIDC_ISSUERS
171
-
172
171
  verifier = Verifier.new key_source: oidc_key_source,
173
172
  aud: aud,
174
173
  azp: azp,
@@ -198,15 +197,14 @@ module Google
198
197
  # checking is performed. Default is to check against {IAP_ISSUERS}.
199
198
  #
200
199
  # @return [Hash] The decoded token payload.
201
- # @raise [KeySourceError] if the key source failed to obtain public keys
202
- # @raise [VerificationError] if the token verification failed.
200
+ # @raise [Google::Auth::IDTokens::KeySourceError] if the key source failed to obtain public keys
201
+ # @raise [Google::Auth::IDTokens::VerificationError] if the token verification failed.
203
202
  # Additional data may be available in the error subclass and message.
204
203
  #
205
204
  def verify_iap token,
206
205
  aud: nil,
207
206
  azp: nil,
208
207
  iss: IAP_ISSUERS
209
-
210
208
  verifier = Verifier.new key_source: iap_key_source,
211
209
  aud: aud,
212
210
  azp: azp,
@@ -0,0 +1,329 @@
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/base_client"
16
+ require "googleauth/errors"
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
+ # The principal behind the credentials. This class allows custom source credentials type
195
+ # that might not implement `principal`, in which case `:unknown` is returned.
196
+ #
197
+ # @private
198
+ # @return [String, Symbol] The string representation of the principal,
199
+ # the token type in lieu of the principal, or :unknown if source principal is unknown.
200
+ def principal
201
+ if @source_credentials.respond_to? :principal
202
+ @source_credentials.principal
203
+ else
204
+ :unknown
205
+ end
206
+ end
207
+
208
+ private
209
+
210
+ # Generates a new impersonation access token by exchanging the source credentials' token
211
+ # at the impersonation URL.
212
+ #
213
+ # This method first fetches an access token from the source credentials and then exchanges it
214
+ # for an impersonation token using the specified impersonation URL. The generated token and
215
+ # its expiration time are cached for subsequent use.
216
+ #
217
+ # @private
218
+ # @param _options [Hash] (optional) Additional options for token retrieval (currently unused).
219
+ #
220
+ # @raise [Google::Auth::UnexpectedStatusError] If the response status is 403 or 500.
221
+ # @raise [Google::Auth::AuthorizationError] For other unexpected response statuses.
222
+ #
223
+ # @return [String] The newly generated impersonation access token.
224
+ def fetch_access_token! _options = {}
225
+ auth_header = prepare_auth_header
226
+ resp = make_impersonation_request auth_header
227
+
228
+ case resp.status
229
+ when 200
230
+ response = MultiJson.load resp.body
231
+ self.expires_at = response["expireTime"]
232
+ @access_token = response["accessToken"]
233
+ access_token
234
+ when 403, 500
235
+ handle_error_response resp, UnexpectedStatusError
236
+ else
237
+ handle_error_response resp, AuthorizationError
238
+ end
239
+ end
240
+
241
+ # Prepares the authorization header for the impersonation request
242
+ # by fetching a token from source credentials.
243
+ #
244
+ # @private
245
+ # @return [Hash] The authorization header with the source credentials' token
246
+ def prepare_auth_header
247
+ auth_header = {}
248
+ @source_credentials.updater_proc.call auth_header
249
+ auth_header
250
+ end
251
+
252
+ # Makes the HTTP request to the impersonation endpoint.
253
+ #
254
+ # @private
255
+ # @param [Hash] auth_header The authorization header containing the source token
256
+ # @return [Faraday::Response] The HTTP response from the impersonation endpoint
257
+ def make_impersonation_request auth_header
258
+ connection.post @impersonation_url do |req|
259
+ req.headers.merge! auth_header
260
+ req.headers["Content-Type"] = "application/json"
261
+ req.body = MultiJson.dump({ scope: @scope })
262
+ end
263
+ end
264
+
265
+ # Creates and raises an appropriate error based on the response.
266
+ #
267
+ # @private
268
+ # @param [Faraday::Response] resp The HTTP response
269
+ # @param [Class] error_class The error class to instantiate
270
+ # @raise [StandardError] The appropriate error with details
271
+ def handle_error_response resp, error_class
272
+ msg = "Unexpected error code #{resp.status}.\n #{resp.env.response_body} #{ERROR_SUFFIX}"
273
+ raise error_class.with_details(
274
+ msg,
275
+ credential_type_name: self.class.name,
276
+ principal: principal
277
+ )
278
+ end
279
+
280
+ # Setter for the expires_at value that makes sure it is converted
281
+ # to Time object.
282
+ def expires_at= new_expires_at
283
+ @expires_at = normalize_timestamp new_expires_at
284
+ end
285
+
286
+ # Returns the type of token (access_token).
287
+ # This method is needed for BaseClient.
288
+ def token_type
289
+ :access_token
290
+ end
291
+
292
+ # Normalizes a timestamp to a Time object.
293
+ #
294
+ # @param time [Time, String, nil] The timestamp to normalize.
295
+ #
296
+ # @return [Time, nil] The normalized Time object, or nil if the input is nil.
297
+ #
298
+ # @raise [Google::Auth::CredentialsError] If the input is not a Time, String, or nil.
299
+ def normalize_timestamp time
300
+ case time
301
+ when NilClass
302
+ nil
303
+ when Time
304
+ time
305
+ when String
306
+ Time.parse time
307
+ else
308
+ message = "Invalid time value #{time}"
309
+ raise CredentialsError.with_details(message, credential_type_name: self.class.name, principal: principal)
310
+ end
311
+ end
312
+
313
+ # Convert all keys in this hash (nested) to symbols for uniform retrieval
314
+ def recursive_hash_normalize_keys val
315
+ if val.is_a? Hash
316
+ deep_hash_normalize val
317
+ else
318
+ val
319
+ end
320
+ end
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
+ end
328
+ end
329
+ end