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
@@ -12,6 +12,8 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ require "google/logging/message"
16
+
15
17
  module Google
16
18
  # Module Auth provides classes that provide Google-specific authorization
17
19
  # used to access Google APIs.
@@ -29,7 +31,14 @@ module Google
29
31
  # fetch the access token there is currently not one, or if the client
30
32
  # has expired
31
33
  fetch_access_token! opts if needs_access_token?
32
- a_hash[AUTH_METADATA_KEY] = "Bearer #{send token_type}"
34
+ token = send token_type
35
+ a_hash[AUTH_METADATA_KEY] = "Bearer #{token}"
36
+ logger&.debug do
37
+ hash = Digest::SHA256.hexdigest token
38
+ Google::Logging::Message.from message: "Sending auth token. (sha256:#{hash})"
39
+ end
40
+
41
+ a_hash[AUTH_METADATA_KEY]
33
42
  end
34
43
 
35
44
  # Returns a clone of a_hash updated with the authentication token
@@ -63,17 +72,25 @@ module Google
63
72
  end
64
73
 
65
74
  def expires_within?
66
- raise NotImplementedError
75
+ raise NoMethodError, "expires_within? not implemented"
76
+ end
77
+
78
+ # The logger used to log operations on this client, such as token refresh.
79
+ attr_accessor :logger
80
+
81
+ # @private
82
+ def principal
83
+ raise NoMethodError, "principal not implemented"
67
84
  end
68
85
 
69
86
  private
70
87
 
71
88
  def token_type
72
- raise NotImplementedError
89
+ raise NoMethodError, "token_type not implemented"
73
90
  end
74
91
 
75
92
  def fetch_access_token!
76
- raise NotImplementedError
93
+ raise NoMethodError, "fetch_access_token! not implemented"
77
94
  end
78
95
  end
79
96
  end
@@ -0,0 +1,162 @@
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "googleauth/base_client"
16
+ require "googleauth/errors"
17
+
18
+ module Google
19
+ module Auth
20
+ ##
21
+ # Implementation of Bearer Token authentication scenario.
22
+ #
23
+ # Bearer tokens are strings representing an authorization grant.
24
+ # They can be OAuth2 ("ya.29") tokens, JWTs, IDTokens -- anything
25
+ # that is sent as a `Bearer` in an `Authorization` header.
26
+ #
27
+ # Not all 'authentication' strings can be used with this class,
28
+ # e.g. an API key cannot since API keys are sent in a
29
+ # `x-goog-api-key` header or as a query parameter.
30
+ #
31
+ # This class should be used when the end-user is managing the
32
+ # authentication token separately, e.g. with a separate service.
33
+ # This means that tasks like tracking the lifetime of and
34
+ # refreshing the token are outside the scope of this class.
35
+ #
36
+ # There is no JSON representation for this type of credentials.
37
+ # If the end-user has credentials in JSON format they should typically
38
+ # use the corresponding credentials type, e.g. ServiceAccountCredentials
39
+ # with the service account JSON.
40
+ #
41
+ class BearerTokenCredentials
42
+ include Google::Auth::BaseClient
43
+
44
+ # @private Authorization header name
45
+ AUTH_METADATA_KEY = Google::Auth::BaseClient::AUTH_METADATA_KEY
46
+
47
+ # @return [String] The token to be sent as a part of Bearer claim
48
+ attr_reader :token
49
+ # The following aliasing is needed for BaseClient since it sends :token_type
50
+ alias bearer_token token
51
+
52
+ # @return [Time, nil] The token expiration time provided by the end-user.
53
+ attr_reader :expires_at
54
+
55
+ # @return [String] The universe domain of the universe
56
+ # this token is for
57
+ attr_accessor :universe_domain
58
+
59
+ class << self
60
+ # Create the BearerTokenCredentials.
61
+ #
62
+ # @param [Hash] options The credentials options
63
+ # @option options [String] :token The bearer token to use.
64
+ # @option options [Time, Numeric, nil] :expires_at The token expiration time provided by the end-user.
65
+ # Optional, for the end-user's convenience. Can be a Time object, a number of seconds since epoch.
66
+ # If `expires_at` is `nil`, it is treated as "token never expires".
67
+ # @option options [String] :universe_domain The universe domain of the universe
68
+ # this token is for (defaults to googleapis.com)
69
+ # @return [Google::Auth::BearerTokenCredentials]
70
+ def make_creds options = {}
71
+ new options
72
+ end
73
+ end
74
+
75
+ # Initialize the BearerTokenCredentials.
76
+ #
77
+ # @param [Hash] options The credentials options
78
+ # @option options [String] :token The bearer token to use.
79
+ # @option options [Time, Numeric, nil] :expires_at The token expiration time provided by the end-user.
80
+ # Optional, for the end-user's convenience. Can be a Time object, a number of seconds since epoch.
81
+ # If `expires_at` is `nil`, it is treated as "token never expires".
82
+ # @option options [String] :universe_domain The universe domain of the universe
83
+ # this token is for (defaults to googleapis.com)
84
+ # @raise [ArgumentError] If the bearer token is nil or empty
85
+ def initialize options = {}
86
+ raise ArgumentError, "Bearer token must be provided" if options[:token].nil? || options[:token].empty?
87
+ @token = options[:token]
88
+ @expires_at = case options[:expires_at]
89
+ when Time
90
+ options[:expires_at]
91
+ when Numeric
92
+ Time.at options[:expires_at]
93
+ end
94
+
95
+ @universe_domain = options[:universe_domain] || "googleapis.com"
96
+ end
97
+
98
+ # Determines if the credentials object has expired.
99
+ #
100
+ # @param [Numeric] seconds The optional timeout in seconds.
101
+ # @return [Boolean] True if the token has expired, false otherwise, or
102
+ # if the expires_at was not provided.
103
+ def expires_within? seconds
104
+ return false if @expires_at.nil? # Treat nil expiration as "never expires"
105
+ Time.now + seconds >= @expires_at
106
+ end
107
+
108
+ # Creates a duplicate of these credentials.
109
+ #
110
+ # @param [Hash] options Additional options for configuring the credentials
111
+ # @option options [String] :token The bearer token to use.
112
+ # @option options [Time, Numeric] :expires_at The token expiration time. Can be a Time
113
+ # object or a number of seconds since epoch.
114
+ # @option options [String] :universe_domain The universe domain (defaults to googleapis.com)
115
+ # @return [Google::Auth::BearerTokenCredentials]
116
+ def duplicate options = {}
117
+ self.class.new(
118
+ token: options[:token] || @token,
119
+ expires_at: options[:expires_at] || @expires_at,
120
+ universe_domain: options[:universe_domain] || @universe_domain
121
+ )
122
+ end
123
+
124
+ # For credentials that are initialized with a token without a principal,
125
+ # the type of that token should be returned as a principal instead
126
+ # @private
127
+ # @return [Symbol] the token type in lieu of the principal
128
+ def principal
129
+ token_type
130
+ end
131
+
132
+ protected
133
+
134
+ ##
135
+ # BearerTokenCredentials do not support fetching a new token.
136
+ #
137
+ # If the token has an expiration time and is expired, this method will
138
+ # raise an error.
139
+ #
140
+ # @param [Hash] _options Options for fetching a new token (not used).
141
+ # @return [nil] Always returns nil.
142
+ # @raise [Google::Auth::CredentialsError] If the token is expired.
143
+ def fetch_access_token! _options = {}
144
+ if @expires_at && Time.now >= @expires_at
145
+ raise CredentialsError.with_details(
146
+ "Bearer token has expired.",
147
+ credential_type_name: self.class.name,
148
+ principal: principal
149
+ )
150
+ end
151
+
152
+ nil
153
+ end
154
+
155
+ private
156
+
157
+ def token_type
158
+ :bearer_token
159
+ end
160
+ end
161
+ end
162
+ end
@@ -14,6 +14,7 @@
14
14
 
15
15
  require "multi_json"
16
16
  require "googleauth/credentials_loader"
17
+ require "googleauth/errors"
17
18
 
18
19
  module Google
19
20
  module Auth
@@ -62,11 +63,11 @@ module Google
62
63
  # @note Direct instantiation is discouraged to avoid embedding IDs
63
64
  # and secrets in source. See {#from_file} to load from
64
65
  # `client_secrets.json` files.
66
+ # @raise [Google::Auth::InitializationError] If id or secret is nil
65
67
  #
66
68
  def initialize id, secret
67
- CredentialsLoader.warn_if_cloud_sdk_credentials id
68
- raise "Client id can not be nil" if id.nil?
69
- raise "Client secret can not be nil" if secret.nil?
69
+ raise InitializationError, "Client id can not be nil" if id.nil?
70
+ raise InitializationError, "Client secret can not be nil" if secret.nil?
70
71
  @id = id
71
72
  @secret = secret
72
73
  end
@@ -78,9 +79,10 @@ module Google
78
79
  # @param [String, File] file
79
80
  # Path of file to read from
80
81
  # @return [Google::Auth::ClientID]
82
+ # @raise [Google::Auth::InitializationError] If file is nil
81
83
  #
82
84
  def self.from_file file
83
- raise "File can not be nil." if file.nil?
85
+ raise InitializationError, "File can not be nil." if file.nil?
84
86
  File.open file.to_s do |f|
85
87
  json = f.read
86
88
  config = MultiJson.load json
@@ -95,11 +97,12 @@ module Google
95
97
  # @param [hash] config
96
98
  # Parsed contents of the JSON file
97
99
  # @return [Google::Auth::ClientID]
100
+ # @raise [Google::Auth::InitializationError] If config is nil or missing required elements
98
101
  #
99
102
  def self.from_hash config
100
- raise "Hash can not be nil." if config.nil?
103
+ raise InitializationError, "Hash can not be nil." if config.nil?
101
104
  raw_detail = config[INSTALLED_APP] || config[WEB_APP]
102
- raise MISSING_TOP_LEVEL_ELEMENT_ERROR if raw_detail.nil?
105
+ raise InitializationError, MISSING_TOP_LEVEL_ELEMENT_ERROR if raw_detail.nil?
103
106
  ClientId.new raw_detail[CLIENT_ID], raw_detail[CLIENT_SECRET]
104
107
  end
105
108
  end
@@ -12,7 +12,8 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- require "faraday"
15
+ require "google-cloud-env"
16
+ require "googleauth/errors"
16
17
  require "googleauth/signet"
17
18
 
18
19
  module Google
@@ -33,93 +34,274 @@ module Google
33
34
  # Extends Signet::OAuth2::Client so that the auth token is obtained from
34
35
  # the GCE metadata server.
35
36
  class GCECredentials < Signet::OAuth2::Client
36
- # The IP Address is used in the URIs to speed up failures on non-GCE
37
- # systems.
37
+ # @private Unused and deprecated but retained to prevent breaking changes
38
38
  DEFAULT_METADATA_HOST = "169.254.169.254".freeze
39
39
 
40
- # @private Unused and deprecated
40
+ # @private Unused and deprecated but retained to prevent breaking changes
41
41
  COMPUTE_AUTH_TOKEN_URI =
42
42
  "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token".freeze
43
- # @private Unused and deprecated
43
+ # @private Unused and deprecated but retained to prevent breaking changes
44
44
  COMPUTE_ID_TOKEN_URI =
45
45
  "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/identity".freeze
46
- # @private Unused and deprecated
46
+ # @private Unused and deprecated but retained to prevent breaking changes
47
47
  COMPUTE_CHECK_URI = "http://169.254.169.254".freeze
48
48
 
49
- @on_gce_cache = {}
50
-
51
49
  class << self
50
+ # @private Unused and deprecated
52
51
  def metadata_host
53
52
  ENV.fetch "GCE_METADATA_HOST", DEFAULT_METADATA_HOST
54
53
  end
55
54
 
55
+ # @private Unused and deprecated
56
56
  def compute_check_uri
57
57
  "http://#{metadata_host}".freeze
58
58
  end
59
59
 
60
+ # @private Unused and deprecated
60
61
  def compute_auth_token_uri
61
62
  "#{compute_check_uri}/computeMetadata/v1/instance/service-accounts/default/token".freeze
62
63
  end
63
64
 
65
+ # @private Unused and deprecated
64
66
  def compute_id_token_uri
65
67
  "#{compute_check_uri}/computeMetadata/v1/instance/service-accounts/default/identity".freeze
66
68
  end
67
69
 
68
70
  # Detect if this appear to be a GCE instance, by checking if metadata
69
71
  # is available.
70
- def on_gce? options = {}, reload = false # rubocop:disable Style/OptionalBooleanParameter
71
- # We can follow OptionalBooleanParameter here because it's a public interface, we can't change it.
72
- @on_gce_cache.delete options if reload
73
- @on_gce_cache.fetch options do
74
- @on_gce_cache[options] = begin
75
- # TODO: This should use google-cloud-env instead.
76
- c = options[:connection] || Faraday.default_connection
77
- headers = { "Metadata-Flavor" => "Google" }
78
- resp = c.get compute_check_uri, nil, headers do |req|
79
- req.options.timeout = 1.0
80
- req.options.open_timeout = 0.1
81
- end
82
- return false unless resp.status == 200
83
- resp.headers["Metadata-Flavor"] == "Google"
84
- rescue Faraday::TimeoutError, Faraday::ConnectionFailed
85
- false
86
- end
87
- end
72
+ # The parameters are deprecated and unused.
73
+ def on_gce? _options = {}, _reload = false # rubocop:disable Style/OptionalBooleanParameter
74
+ Google::Cloud.env.metadata?
88
75
  end
89
76
 
90
77
  def reset_cache
91
- @on_gce_cache.clear
78
+ Google::Cloud.env.compute_metadata.reset_existence!
79
+ Google::Cloud.env.compute_metadata.cache.expire_all!
92
80
  end
93
81
  alias unmemoize_all reset_cache
94
82
  end
95
83
 
84
+ # @private Temporary; remove when universe domain metadata endpoint is stable (see b/349488459).
85
+ attr_accessor :disable_universe_domain_check
86
+
87
+ # Construct a GCECredentials
88
+ def initialize options = {}
89
+ # Override the constructor to remember whether the universe domain was
90
+ # overridden by a constructor argument.
91
+ @universe_domain_overridden = options["universe_domain"] || options[:universe_domain]
92
+ # TODO: Remove when universe domain metadata endpoint is stable (see b/349488459).
93
+ @disable_universe_domain_check = true
94
+ super options
95
+ end
96
+
97
+ # Creates a duplicate of these credentials
98
+ # without the Signet::OAuth2::Client-specific
99
+ # transient state (e.g. cached tokens)
100
+ #
101
+ # @param options [Hash] Overrides for the credentials parameters.
102
+ # The following keys are recognized in addition to keys in the
103
+ # Signet::OAuth2::Client
104
+ # * `:universe_domain_overridden` Whether the universe domain was
105
+ # overriden during credentials creation
106
+ def duplicate options = {}
107
+ options = deep_hash_normalize options
108
+ super(
109
+ {
110
+ universe_domain_overridden: @universe_domain_overridden
111
+ }.merge(options)
112
+ )
113
+ end
114
+
115
+ # @private
116
+ # Overrides universe_domain getter to fetch lazily if it hasn't been
117
+ # fetched yet. This is necessary specifically for Compute Engine because
118
+ # the universe comes from the metadata service, and isn't known
119
+ # immediately on credential construction. All other credential types read
120
+ # the universe from their json key or other immediate input.
121
+ def universe_domain
122
+ value = super
123
+ return value unless value.nil?
124
+ fetch_access_token!
125
+ super
126
+ end
127
+
96
128
  # Overrides the super class method to change how access tokens are
97
129
  # fetched.
98
- def fetch_access_token options = {}
99
- c = options[:connection] || Faraday.default_connection
100
- retry_with_error do
101
- uri = target_audience ? GCECredentials.compute_id_token_uri : GCECredentials.compute_auth_token_uri
102
- query = target_audience ? { "audience" => target_audience, "format" => "full" } : {}
103
- query[:scopes] = Array(scope).join "," if scope
104
- resp = c.get uri, query, "Metadata-Flavor" => "Google"
105
- case resp.status
106
- when 200
107
- content_type = resp.headers["content-type"]
108
- if ["text/html", "application/text"].include? content_type
109
- { (target_audience ? "id_token" : "access_token") => resp.body }
110
- else
111
- Signet::OAuth2.parse_credentials resp.body, content_type
112
- end
113
- when 403, 500
114
- msg = "Unexpected error code #{resp.status} #{UNEXPECTED_ERROR_SUFFIX}"
115
- raise Signet::UnexpectedStatusError, msg
116
- when 404
117
- raise Signet::AuthorizationError, NO_METADATA_SERVER_ERROR
130
+ #
131
+ # @param [Hash] _options Options for token fetch (not used)
132
+ # @return [Hash] The token data hash
133
+ # @raise [Google::Auth::UnexpectedStatusError] On unexpected HTTP status codes
134
+ # @raise [Google::Auth::AuthorizationError] If metadata server is unavailable or returns error
135
+ def fetch_access_token _options = {}
136
+ query, entry = build_metadata_request_params
137
+ begin
138
+ log_fetch_query
139
+ resp = Google::Cloud.env.lookup_metadata_response "instance", entry, query: query
140
+ log_fetch_resp resp
141
+ handle_metadata_response resp
142
+ rescue Google::Cloud::Env::MetadataServerNotResponding => e
143
+ log_fetch_err e
144
+ raise AuthorizationError.with_details(
145
+ e.message,
146
+ credential_type_name: self.class.name,
147
+ principal: principal
148
+ )
149
+ end
150
+ end
151
+
152
+ # Destructively updates these credentials.
153
+ #
154
+ # This method is called by `Signet::OAuth2::Client`'s constructor
155
+ #
156
+ # @param options [Hash] Overrides for the credentials parameters.
157
+ # The following keys are recognized in addition to keys in the
158
+ # Signet::OAuth2::Client
159
+ # * `:universe_domain_overridden` Whether the universe domain was
160
+ # overriden during credentials creation
161
+ # @return [Google::Auth::GCECredentials]
162
+ def update! options = {}
163
+ # Normalize all keys to symbols to allow indifferent access.
164
+ options = deep_hash_normalize options
165
+
166
+ @universe_domain_overridden = options[:universe_domain_overridden] if options.key? :universe_domain_overridden
167
+
168
+ super(options)
169
+
170
+ self
171
+ end
172
+
173
+ # Returns the principal identifier for GCE credentials
174
+ # @private
175
+ # @return [Symbol] :gce to represent Google Compute Engine identity
176
+ def principal
177
+ :gce_metadata
178
+ end
179
+
180
+ private
181
+
182
+ # @private
183
+ # Builds query parameters and endpoint for metadata request
184
+ # @return [Array] The query parameters and endpoint path
185
+ def build_metadata_request_params
186
+ query, entry =
187
+ if token_type == :id_token
188
+ [{ "audience" => target_audience, "format" => "full" }, "service-accounts/default/identity"]
189
+ else
190
+ [{}, "service-accounts/default/token"]
191
+ end
192
+ query[:scopes] = Array(scope).join "," if scope
193
+ [query, entry]
194
+ end
195
+
196
+ # @private
197
+ # Handles the response from the metadata server
198
+ # @param [Google::Cloud::Env::MetadataResponse] resp The metadata server response
199
+ # @return [Hash] The token hash on success
200
+ # @raise [Google::Auth::UnexpectedStatusError, Google::Auth::AuthorizationError] On error
201
+ def handle_metadata_response resp
202
+ case resp.status
203
+ when 200
204
+ build_token_hash resp.body, resp.headers["content-type"], resp.retrieval_monotonic_time
205
+ when 403, 500
206
+ raise Signet::UnexpectedStatusError, "Unexpected error code #{resp.status} #{UNEXPECTED_ERROR_SUFFIX}"
207
+ when 404
208
+ raise Signet::AuthorizationError, NO_METADATA_SERVER_ERROR
209
+ else
210
+ raise Signet::AuthorizationError, "Unexpected error code #{resp.status} #{UNEXPECTED_ERROR_SUFFIX}"
211
+ end
212
+ end
213
+
214
+ def log_fetch_query
215
+ if token_type == :id_token
216
+ logger&.info do
217
+ Google::Logging::Message.from(
218
+ message: "Requesting id token from MDS with aud=#{target_audience}",
219
+ "credentialsId" => object_id
220
+ )
221
+ end
222
+ else
223
+ logger&.info do
224
+ Google::Logging::Message.from(
225
+ message: "Requesting access token from MDS",
226
+ "credentialsId" => object_id
227
+ )
228
+ end
229
+ end
230
+ end
231
+
232
+ def log_fetch_resp resp
233
+ logger&.info do
234
+ Google::Logging::Message.from(
235
+ message: "Received #{resp.status} from MDS",
236
+ "credentialsId" => object_id
237
+ )
238
+ end
239
+ end
240
+
241
+ def log_fetch_err _err
242
+ logger&.info do
243
+ Google::Logging::Message.from(
244
+ message: "MDS did not respond to token request",
245
+ "credentialsId" => object_id
246
+ )
247
+ end
248
+ end
249
+
250
+ # Constructs a token hash from the metadata server response
251
+ #
252
+ # @private
253
+ # @param [String] body The response body from the metadata server
254
+ # @param [String] content_type The content type of the response
255
+ # @param [Float] retrieval_time The monotonic time when the response was retrieved
256
+ #
257
+ # @return [Hash] A hash containing:
258
+ # - access_token/id_token: The actual token depending on what was requested
259
+ # - token_type: The type of token (usually "Bearer")
260
+ # - expires_in: Seconds until token expiration (adjusted for freshness)
261
+ # - universe_domain: The universe domain for the token (if not overridden)
262
+ def build_token_hash body, content_type, retrieval_time
263
+ hash =
264
+ if ["text/html", "application/text"].include? content_type
265
+ parse_encoded_token body
118
266
  else
119
- msg = "Unexpected error code #{resp.status} #{UNEXPECTED_ERROR_SUFFIX}"
120
- raise Signet::AuthorizationError, msg
267
+ Signet::OAuth2.parse_credentials body, content_type
121
268
  end
269
+ add_universe_domain_to hash
270
+ adjust_for_stale_expires_in hash, retrieval_time
271
+ hash
272
+ end
273
+
274
+ def parse_encoded_token body
275
+ hash = { token_type.to_s => body }
276
+ if token_type == :id_token
277
+ expires_at = expires_at_from_id_token body
278
+ hash["expires_at"] = expires_at if expires_at
122
279
  end
280
+ hash
281
+ end
282
+
283
+ def add_universe_domain_to hash
284
+ return if @universe_domain_overridden
285
+ universe_domain =
286
+ if disable_universe_domain_check
287
+ # TODO: Remove when universe domain metadata endpoint is stable (see b/349488459).
288
+ "googleapis.com"
289
+ else
290
+ Google::Cloud.env.lookup_metadata "universe", "universe-domain"
291
+ end
292
+ universe_domain = "googleapis.com" if !universe_domain || universe_domain.empty?
293
+ hash["universe_domain"] = universe_domain.strip
294
+ end
295
+
296
+ # The response might have been cached, which means expires_in might be
297
+ # stale. Update it based on the time since the data was retrieved.
298
+ # We also ensure expires_in is conservative; subtracting at least 1
299
+ # second to offset any skew from metadata server latency.
300
+ def adjust_for_stale_expires_in hash, retrieval_time
301
+ return unless hash["expires_in"].is_a? Numeric
302
+ offset = 1 + (Process.clock_gettime(Process::CLOCK_MONOTONIC) - retrieval_time).round
303
+ hash["expires_in"] -= offset if offset.positive?
304
+ hash["expires_in"] = 0 if hash["expires_in"].negative?
123
305
  end
124
306
  end
125
307
  end