googleauth 1.14.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/Credentials.md +106 -0
  4. data/Errors.md +152 -0
  5. data/lib/googleauth/api_key.rb +9 -0
  6. data/lib/googleauth/application_default.rb +3 -1
  7. data/lib/googleauth/base_client.rb +5 -0
  8. data/lib/googleauth/bearer_token.rb +16 -2
  9. data/lib/googleauth/client_id.rb +9 -5
  10. data/lib/googleauth/compute_engine.rb +64 -18
  11. data/lib/googleauth/credentials.rb +23 -6
  12. data/lib/googleauth/credentials_loader.rb +9 -4
  13. data/lib/googleauth/default_credentials.rb +16 -4
  14. data/lib/googleauth/errors.rb +117 -0
  15. data/lib/googleauth/external_account/aws_credentials.rb +85 -18
  16. data/lib/googleauth/external_account/base_credentials.rb +31 -2
  17. data/lib/googleauth/external_account/external_account_utils.rb +15 -4
  18. data/lib/googleauth/external_account/identity_pool_credentials.rb +40 -15
  19. data/lib/googleauth/external_account/pluggable_credentials.rb +34 -19
  20. data/lib/googleauth/external_account.rb +30 -6
  21. data/lib/googleauth/iam.rb +19 -3
  22. data/lib/googleauth/id_tokens/errors.rb +13 -7
  23. data/lib/googleauth/id_tokens/key_sources.rb +13 -7
  24. data/lib/googleauth/id_tokens/verifier.rb +2 -3
  25. data/lib/googleauth/id_tokens.rb +4 -4
  26. data/lib/googleauth/impersonated_service_account.rb +64 -17
  27. data/lib/googleauth/json_key_reader.rb +11 -2
  28. data/lib/googleauth/oauth2/sts_client.rb +9 -4
  29. data/lib/googleauth/scope_util.rb +1 -1
  30. data/lib/googleauth/service_account.rb +12 -1
  31. data/lib/googleauth/service_account_jwt_header.rb +9 -2
  32. data/lib/googleauth/signet.rb +24 -6
  33. data/lib/googleauth/user_authorizer.rb +35 -7
  34. data/lib/googleauth/user_refresh.rb +25 -7
  35. data/lib/googleauth/version.rb +1 -1
  36. data/lib/googleauth/web_user_authorizer.rb +46 -9
  37. data/lib/googleauth.rb +1 -0
  38. metadata +8 -5
@@ -15,6 +15,8 @@
15
15
  require "os"
16
16
  require "rbconfig"
17
17
 
18
+ require "googleauth/errors"
19
+
18
20
  module Google
19
21
  # Module Auth provides classes that provide Google-specific authorization
20
22
  # used to access Google APIs.
@@ -71,11 +73,12 @@ module Google
71
73
  # The following keys are recognized:
72
74
  # * `:default_connection` The connection object to use.
73
75
  # * `:connection_builder` A `Proc` that returns a connection.
76
+ # @raise [Google::Auth::InitializationError] If the credentials file cannot be read
74
77
  def from_env scope = nil, options = {}
75
78
  options = interpret_options scope, options
76
79
  if ENV.key?(ENV_VAR) && !ENV[ENV_VAR].empty?
77
80
  path = ENV[ENV_VAR]
78
- raise "file #{path} does not exist" unless File.exist? path
81
+ raise InitializationError, "file #{path} does not exist" unless File.exist? path
79
82
  File.open path do |f|
80
83
  return make_creds options.merge(json_key_io: f)
81
84
  end
@@ -83,7 +86,7 @@ module Google
83
86
  make_creds options
84
87
  end
85
88
  rescue StandardError => e
86
- raise "#{NOT_FOUND_ERROR}: #{e}"
89
+ raise InitializationError, "#{NOT_FOUND_ERROR}: #{e}"
87
90
  end
88
91
 
89
92
  # Creates an instance from a well known path.
@@ -97,6 +100,7 @@ module Google
97
100
  # The following keys are recognized:
98
101
  # * `:default_connection` The connection object to use.
99
102
  # * `:connection_builder` A `Proc` that returns a connection.
103
+ # @raise [Google::Auth::InitializationError] If the credentials file cannot be read
100
104
  def from_well_known_path scope = nil, options = {}
101
105
  options = interpret_options scope, options
102
106
  home_var = OS.windows? ? "APPDATA" : "HOME"
@@ -109,7 +113,7 @@ module Google
109
113
  return make_creds options.merge(json_key_io: f)
110
114
  end
111
115
  rescue StandardError => e
112
- raise "#{WELL_KNOWN_ERROR}: #{e}"
116
+ raise InitializationError, "#{WELL_KNOWN_ERROR}: #{e}"
113
117
  end
114
118
 
115
119
  # Creates an instance from the system default path
@@ -123,6 +127,7 @@ module Google
123
127
  # The following keys are recognized:
124
128
  # * `:default_connection` The connection object to use.
125
129
  # * `:connection_builder` A `Proc` that returns a connection.
130
+ # @raise [Google::Auth::InitializationError] If the credentials file cannot be read or is invalid
126
131
  def from_system_default_path scope = nil, options = {}
127
132
  options = interpret_options scope, options
128
133
  if OS.windows?
@@ -137,7 +142,7 @@ module Google
137
142
  return make_creds options.merge(json_key_io: f)
138
143
  end
139
144
  rescue StandardError => e
140
- raise "#{SYSTEM_DEFAULT_ERROR}: #{e}"
145
+ raise InitializationError, "#{SYSTEM_DEFAULT_ERROR}: #{e}"
141
146
  end
142
147
 
143
148
  module_function
@@ -16,6 +16,7 @@ require "multi_json"
16
16
  require "stringio"
17
17
 
18
18
  require "googleauth/credentials_loader"
19
+ require "googleauth/errors"
19
20
  require "googleauth/external_account"
20
21
  require "googleauth/service_account"
21
22
  require "googleauth/service_account_jwt_header"
@@ -42,6 +43,9 @@ module Google
42
43
  # information, refer to [Validate credential configurations from external
43
44
  # sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
44
45
  #
46
+ # @param options [Hash] Options for creating the credentials
47
+ # @return [Google::Auth::Credentials] The credentials instance
48
+ # @raise [Google::Auth::InitializationError] If the credentials cannot be determined
45
49
  def self.make_creds options = {}
46
50
  json_key_io = options[:json_key_io]
47
51
  if json_key_io
@@ -54,10 +58,14 @@ module Google
54
58
  end
55
59
  end
56
60
 
61
+ # Reads the credential type from environment and returns the appropriate class
62
+ #
63
+ # @return [Class] The credential class to use
64
+ # @raise [Google::Auth::InitializationError] If the credentials type is undefined or unsupported
57
65
  def self.read_creds
58
66
  env_var = CredentialsLoader::ACCOUNT_TYPE_VAR
59
67
  type = ENV[env_var]
60
- raise "#{env_var} is undefined in env" unless type
68
+ raise InitializationError, "#{env_var} is undefined in env" unless type
61
69
  case type
62
70
  when "service_account"
63
71
  ServiceAccountCredentials
@@ -66,15 +74,19 @@ module Google
66
74
  when "external_account"
67
75
  ExternalAccount::Credentials
68
76
  else
69
- raise "credentials type '#{type}' is not supported"
77
+ raise InitializationError, "credentials type '#{type}' is not supported"
70
78
  end
71
79
  end
72
80
 
73
81
  # Reads the input json and determines which creds class to use.
82
+ #
83
+ # @param json_key_io [IO] An IO object containing the JSON key
84
+ # @return [Array(Hash, Class)] The JSON key and the credential class to use
85
+ # @raise [Google::Auth::InitializationError] If the JSON is missing the type field or has an unsupported type
74
86
  def self.determine_creds_class json_key_io
75
87
  json_key = MultiJson.load json_key_io.read
76
88
  key = "type"
77
- raise "the json is missing the '#{key}' field" unless json_key.key? key
89
+ raise InitializationError, "the json is missing the '#{key}' field" unless json_key.key? key
78
90
  type = json_key[key]
79
91
  case type
80
92
  when "service_account"
@@ -84,7 +96,7 @@ module Google
84
96
  when "external_account"
85
97
  [json_key, ExternalAccount::Credentials]
86
98
  else
87
- raise "credentials type '#{type}' is not supported"
99
+ raise InitializationError, "credentials type '#{type}' is not supported"
88
100
  end
89
101
  end
90
102
  end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "signet/oauth_2/client"
4
+
5
+ # Copyright 2025 Google LLC
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ module Google
20
+ module Auth
21
+ ##
22
+ # Error mixin module for Google Auth errors
23
+ # All Google Auth errors should include this module
24
+ #
25
+ module Error; end
26
+
27
+ ##
28
+ # Mixin module that contains detailed error information
29
+ # typically this is available if credentials initialization
30
+ # succeeds and credentials object is valid
31
+ #
32
+ module DetailedError
33
+ include Error
34
+
35
+ # The type of the credentials that the error was originated from
36
+ # @return [String, nil] The class name of the credential that raised the error
37
+ attr_reader :credential_type_name
38
+
39
+ # The principal for the authentication flow. Typically obtained from credentials
40
+ # @return [String, Symbol, nil] The principal identifier associated with the credentials
41
+ attr_reader :principal
42
+
43
+ # All details passed in the options hash when creating the error
44
+ # @return [Hash] Additional details about the error
45
+ attr_reader :details
46
+
47
+ # @private
48
+ def self.included base
49
+ base.extend ClassMethods
50
+ end
51
+
52
+ # Class methods to be added to including classes
53
+ module ClassMethods
54
+ # Creates a new error with detailed information
55
+ # @param message [String] The error message
56
+ # @param credential_type_name [String] The credential type that raised the error
57
+ # @param principal [String, Symbol] The principal for the authentication flow
58
+ # @return [Error] The new error with details
59
+ def with_details message, credential_type_name:, principal:
60
+ new(message).tap do |error|
61
+ error.instance_variable_set :@credential_type_name, credential_type_name
62
+ error.instance_variable_set :@principal, principal
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ ##
69
+ # Error raised during Credentials initialization.
70
+ # All new code should use this instead of ArgumentError during initializtion.
71
+ #
72
+ class InitializationError < StandardError
73
+ include Error
74
+ end
75
+
76
+ ##
77
+ # Generic error raised during operation of Credentials
78
+ # This should be used for all purposes not covered by other errors.
79
+ #
80
+ class CredentialsError < StandardError
81
+ include DetailedError
82
+ end
83
+
84
+ ##
85
+ # An error indicating the remote server refused to authorize the client.
86
+ # Maintains backward compatibility with Signet.
87
+ #
88
+ # Should not be used in the new code, even when wrapping `Signet::AuthorizationError`.
89
+ # New code should use CredentialsError instead.
90
+ #
91
+ class AuthorizationError < Signet::AuthorizationError
92
+ include DetailedError
93
+ end
94
+
95
+ ##
96
+ # An error indicating that the server sent an unexpected http status.
97
+ # Maintains backward compatibility with Signet.
98
+ #
99
+ # Should not be used in the new code, even when wrapping `Signet::UnexpectedStatusError`.
100
+ # New code should use CredentialsError instead.
101
+ #
102
+ class UnexpectedStatusError < Signet::UnexpectedStatusError
103
+ include DetailedError
104
+ end
105
+
106
+ ##
107
+ # An error indicating the client failed to parse a value.
108
+ # Maintains backward compatibility with Signet.
109
+ #
110
+ # Should not be used in the new code, even when wrapping `Signet::ParseError`.
111
+ # New code should use CredentialsError instead.
112
+ #
113
+ class ParseError < Signet::ParseError
114
+ include DetailedError
115
+ end
116
+ end
117
+ end
@@ -13,6 +13,7 @@
13
13
  # limitations under the License.
14
14
 
15
15
  require "time"
16
+ require "googleauth/errors"
16
17
  require "googleauth/external_account/base_credentials"
17
18
  require "googleauth/external_account/external_account_utils"
18
19
 
@@ -106,17 +107,32 @@ module Google
106
107
 
107
108
  private
108
109
 
110
+ # Retrieves an IMDSv2 session token or returns a cached token if valid
111
+ #
112
+ # @return [String] The IMDSv2 session token
113
+ # @raise [Google::Auth::CredentialsError] If the token URL is missing or there's an error retrieving the token
109
114
  def imdsv2_session_token
110
115
  return @imdsv2_session_token unless imdsv2_session_token_invalid?
111
- raise "IMDSV2 token url must be provided" if @imdsv2_session_token_url.nil?
116
+ if @imdsv2_session_token_url.nil?
117
+ raise CredentialsError.with_details(
118
+ "IMDSV2 token url must be provided",
119
+ credential_type_name: self.class.name,
120
+ principal: principal
121
+ )
122
+ end
112
123
  begin
113
124
  response = connection.put @imdsv2_session_token_url do |req|
114
125
  req.headers["x-aws-ec2-metadata-token-ttl-seconds"] = IMDSV2_TOKEN_EXPIRATION_IN_SECONDS.to_s
115
126
  end
127
+ raise Faraday::Error unless response.success?
116
128
  rescue Faraday::Error => e
117
- raise "Fetching AWS IMDSV2 token error: #{e}"
129
+ raise CredentialsError.with_details(
130
+ "Fetching AWS IMDSV2 token error: #{e}",
131
+ credential_type_name: self.class.name,
132
+ principal: principal
133
+ )
118
134
  end
119
- raise Faraday::Error unless response.success?
135
+
120
136
  @imdsv2_session_token = response.body
121
137
  @imdsv2_session_token_expiry = Time.now + IMDSV2_TOKEN_EXPIRATION_IN_SECONDS
122
138
  @imdsv2_session_token
@@ -127,6 +143,14 @@ module Google
127
143
  @imdsv2_session_token_expiry.nil? || @imdsv2_session_token_expiry < Time.now
128
144
  end
129
145
 
146
+ # Makes a request to an AWS resource endpoint
147
+ #
148
+ # @param [String] url The AWS endpoint URL
149
+ # @param [String] name Resource name for error messages
150
+ # @param [Hash, nil] data Optional data to send in POST requests
151
+ # @param [Hash] headers Optional request headers
152
+ # @return [Faraday::Response] The successful response
153
+ # @raise [Google::Auth::CredentialsError] If the request fails
130
154
  def get_aws_resource url, name, data: nil, headers: {}
131
155
  begin
132
156
  headers["x-aws-ec2-metadata-token"] = imdsv2_session_token
@@ -136,11 +160,14 @@ module Google
136
160
  else
137
161
  connection.get url, nil, headers
138
162
  end
139
-
140
163
  raise Faraday::Error unless response.success?
141
164
  response
142
165
  rescue Faraday::Error
143
- raise "Failed to retrieve AWS #{name}."
166
+ raise CredentialsError.with_details(
167
+ "Failed to retrieve AWS #{name}.",
168
+ credential_type_name: self.class.name,
169
+ principal: principal
170
+ )
144
171
  end
145
172
  end
146
173
 
@@ -181,9 +208,16 @@ module Google
181
208
  # Retrieves the AWS role currently attached to the current AWS workload by querying the AWS metadata server.
182
209
  # This is needed for the AWS metadata server security credentials endpoint in order to retrieve the AWS security
183
210
  # credentials needed to sign requests to AWS APIs.
211
+ #
212
+ # @return [String] The AWS role name
213
+ # @raise [Google::Auth::CredentialsError] If the credential verification URL is not set or if the request fails
184
214
  def fetch_metadata_role_name
185
215
  unless @credential_verification_url
186
- raise "Unable to determine the AWS metadata server security credentials endpoint"
216
+ raise CredentialsError.with_details(
217
+ "Unable to determine the AWS metadata server security credentials endpoint",
218
+ credential_type_name: self.class.name,
219
+ principal: principal
220
+ )
187
221
  end
188
222
 
189
223
  get_aws_resource(@credential_verification_url, "IAM Role").body
@@ -195,11 +229,22 @@ module Google
195
229
  MultiJson.load response.body
196
230
  end
197
231
 
232
+ # Reads the name of the AWS region from the environment
233
+ #
234
+ # @return [String] The name of the AWS region
235
+ # @raise [Google::Auth::CredentialsError] If the region is not set in the environment
236
+ # and the region_url was not set in credentials source
198
237
  def region
199
238
  @region = ENV[CredentialsLoader::AWS_REGION_VAR] || ENV[CredentialsLoader::AWS_DEFAULT_REGION_VAR]
200
239
 
201
240
  unless @region
202
- raise "region_url or region must be set for external account credentials" unless @region_url
241
+ unless @region_url
242
+ raise CredentialsError.with_details(
243
+ "region_url or region must be set for external account credentials",
244
+ credential_type_name: self.class.name,
245
+ principal: principal
246
+ )
247
+ end
203
248
 
204
249
  @region ||= get_aws_resource(@region_url, "region").body[0..-2]
205
250
  end
@@ -220,23 +265,45 @@ module Google
220
265
  @region_name = region_name
221
266
  end
222
267
 
223
- # Generates the signed request for the provided HTTP request for calling
224
- # an AWS API. This follows the steps described at:
268
+ # Generates an AWS signature version 4 signed request.
269
+ #
270
+ # Creates a signed request following the AWS Signature Version 4 process, which
271
+ # provides secure authentication for AWS API calls. The process includes creating
272
+ # canonical request strings, calculating signatures using the AWS credentials, and
273
+ # building proper authorization headers.
274
+ #
275
+ # For detailed information on the signing process, see:
225
276
  # https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
226
277
  #
227
- # @param [Hash[string, string]] aws_security_credentials
228
- # A dictionary containing the AWS security credentials.
229
- # @param [string] url
230
- # The AWS service URL containing the canonical URI and query string.
231
- # @param [string] method
232
- # The HTTP method used to call this API.
278
+ # @param [Hash] aws_credentials The AWS security credentials with the following keys:
279
+ # @option aws_credentials [String] :access_key_id The AWS access key ID
280
+ # @option aws_credentials [String] :secret_access_key The AWS secret access key
281
+ # @option aws_credentials [String, nil] :session_token Optional AWS session token
282
+ # @param [Hash] original_request The request to sign with the following keys:
283
+ # @option original_request [String] :url The AWS service URL (must be HTTPS)
284
+ # @option original_request [String] :method The HTTP method (GET, POST, etc.)
285
+ # @option original_request [Hash, nil] :headers Optional request headers
286
+ # @option original_request [String, nil] :data Optional request payload
287
+ #
288
+ # @return [Hash] The signed request with the following keys:
289
+ # * :url - The original URL as a string
290
+ # * :headers - A hash of headers with the authorization header added
291
+ # * :method - The HTTP method
292
+ # * :data - The request payload (if present)
233
293
  #
234
- # @return [hash{string => string}]
235
- # The AWS signed request dictionary object.
294
+ # @raise [Google::Auth::CredentialsError] If the AWS service URL is invalid
236
295
  #
237
296
  def generate_signed_request aws_credentials, original_request
238
297
  uri = Addressable::URI.parse original_request[:url]
239
- raise "Invalid AWS service URL" unless uri.hostname && uri.scheme == "https"
298
+ unless uri.hostname && uri.scheme == "https"
299
+ # NOTE: We use AwsCredentials name but can't access its principal since AwsRequestSigner
300
+ # is a separate class and not a credential object with access to the audience
301
+ raise CredentialsError.with_details(
302
+ "Invalid AWS service URL",
303
+ credential_type_name: AwsCredentials.name,
304
+ principal: "aws"
305
+ )
306
+ end
240
307
  service_name = uri.host.split(".").first
241
308
 
242
309
  datetime = Time.now.utc.strftime "%Y%m%dT%H%M%SZ"
@@ -13,6 +13,7 @@
13
13
  # limitations under the License.require "time"
14
14
 
15
15
  require "googleauth/base_client"
16
+ require "googleauth/errors"
16
17
  require "googleauth/helpers/connection"
17
18
  require "googleauth/oauth2/sts_client"
18
19
 
@@ -89,6 +90,14 @@ module Google
89
90
  %r{/iam\.googleapis\.com/locations/[^/]+/workforcePools/}.match?(@audience || "")
90
91
  end
91
92
 
93
+ # For external account credentials, the principal is
94
+ # represented by the audience, such as a workforce pool
95
+ # @private
96
+ # @return [String] the GCP principal, e.g. a workforce pool
97
+ def principal
98
+ @audience
99
+ end
100
+
92
101
  private
93
102
 
94
103
  def token_type
@@ -96,6 +105,8 @@ module Google
96
105
  :access_token
97
106
  end
98
107
 
108
+ # A common method for Other credentials to call during initialization
109
+ # @raise [Google::Auth::InitializationError] If workforce_pool_user_project is incorrectly set
99
110
  def base_setup options
100
111
  self.default_connection = options[:connection]
101
112
 
@@ -121,9 +132,11 @@ module Google
121
132
  connection: default_connection
122
133
  )
123
134
  return unless @workforce_pool_user_project && !is_workforce_pool?
124
- raise "workforce_pool_user_project should not be set for non-workforce pool credentials."
135
+ raise InitializationError, "workforce_pool_user_project should not be set for non-workforce pool credentials."
125
136
  end
126
137
 
138
+ # Exchange tokens at STS endpoint
139
+ # @raise [Google::Auth::AuthorizationError] If the token exchange request fails
127
140
  def exchange_token
128
141
  additional_options = nil
129
142
  if @client_id.nil? && @workforce_pool_user_project
@@ -140,6 +153,12 @@ module Google
140
153
  }
141
154
  log_token_request token_request
142
155
  @sts_client.exchange_token token_request
156
+ rescue Google::Auth::AuthorizationError => e
157
+ raise Google::Auth::AuthorizationError.with_details(
158
+ e.message,
159
+ credential_type_name: self.class.name,
160
+ principal: principal
161
+ )
143
162
  end
144
163
 
145
164
  def log_token_request token_request
@@ -160,6 +179,12 @@ module Google
160
179
  end
161
180
  end
162
181
 
182
+ # Exchanges a token for an impersonated service account access token
183
+ #
184
+ # @param [String] token The token to exchange
185
+ # @param [Hash] _options Additional options (not used)
186
+ # @return [Hash] The response containing the impersonated access token
187
+ # @raise [Google::Auth::CredentialsError] If the impersonation request fails
163
188
  def get_impersonated_access_token token, _options = {}
164
189
  log_impersonated_token_request token
165
190
  response = connection.post @service_account_impersonation_url do |req|
@@ -169,7 +194,11 @@ module Google
169
194
  end
170
195
 
171
196
  if response.status != 200
172
- raise "Service account impersonation failed with status #{response.status}"
197
+ raise CredentialsError.with_details(
198
+ "Service account impersonation failed with status #{response.status}",
199
+ credential_type_name: self.class.name,
200
+ principal: principal
201
+ )
173
202
  end
174
203
 
175
204
  MultiJson.load response.body
@@ -13,6 +13,7 @@
13
13
  # limitations under the License.require "time"
14
14
 
15
15
  require "googleauth/base_client"
16
+ require "googleauth/errors"
16
17
  require "googleauth/helpers/connection"
17
18
  require "googleauth/oauth2/sts_client"
18
19
 
@@ -36,8 +37,8 @@ module Google
36
37
  # call this API or the required scopes may not be selected:
37
38
  # https://cloud.google.com/resource-manager/reference/rest/v1/projects/get#authorization-scopes
38
39
  #
39
- # @return [string,nil]
40
- # The project ID corresponding to the workload identity pool or workforce pool if determinable.
40
+ # @return [String, nil] The project ID corresponding to the workload identity
41
+ # pool or workforce pool if determinable
41
42
  #
42
43
  def project_id
43
44
  return @project_id unless @project_id.nil?
@@ -65,7 +66,8 @@ module Google
65
66
  # STS audience pattern:
66
67
  # `//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/...`
67
68
  #
68
- # @return [string, nil]
69
+ # @return [String, nil] The project number extracted from the audience string,
70
+ # or nil if it cannot be determined
69
71
  #
70
72
  def project_number
71
73
  segments = @audience.split "/"
@@ -74,6 +76,11 @@ module Google
74
76
  segments[idx + 1]
75
77
  end
76
78
 
79
+ # Normalizes a timestamp value to a Time object
80
+ #
81
+ # @param time [Time, String, nil] The timestamp to normalize
82
+ # @return [Time, nil] The normalized timestamp or nil if input is nil
83
+ # @raise [Google::Auth::CredentialsError] If the time value is not nil, Time, or String
77
84
  def normalize_timestamp time
78
85
  case time
79
86
  when NilClass
@@ -83,10 +90,14 @@ module Google
83
90
  when String
84
91
  Time.parse time
85
92
  else
86
- raise "Invalid time value #{time}"
93
+ raise CredentialsError, "Invalid time value #{time}"
87
94
  end
88
95
  end
89
96
 
97
+ # Extracts the service account email from the impersonation URL
98
+ #
99
+ # @return [String, nil] The service account email extracted from the
100
+ # service_account_impersonation_url, or nil if it cannot be determined
90
101
  def service_account_email
91
102
  return nil if @service_account_impersonation_url.nil?
92
103
  start_idx = @service_account_impersonation_url.rindex "/"