googleauth 1.14.0 → 1.17.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -0
  3. data/Credentials.md +110 -0
  4. data/Errors.md +152 -0
  5. data/README.md +0 -1
  6. data/lib/googleauth/api_key.rb +9 -0
  7. data/lib/googleauth/application_default.rb +3 -1
  8. data/lib/googleauth/base_client.rb +5 -0
  9. data/lib/googleauth/bearer_token.rb +16 -2
  10. data/lib/googleauth/client_id.rb +15 -7
  11. data/lib/googleauth/compute_engine.rb +64 -18
  12. data/lib/googleauth/credentials.rb +66 -35
  13. data/lib/googleauth/credentials_loader.rb +30 -5
  14. data/lib/googleauth/default_credentials.rb +69 -34
  15. data/lib/googleauth/errors.rb +117 -0
  16. data/lib/googleauth/external_account/aws_credentials.rb +87 -19
  17. data/lib/googleauth/external_account/base_credentials.rb +34 -4
  18. data/lib/googleauth/external_account/external_account_utils.rb +17 -5
  19. data/lib/googleauth/external_account/identity_pool_credentials.rb +42 -16
  20. data/lib/googleauth/external_account/pluggable_credentials.rb +37 -21
  21. data/lib/googleauth/external_account.rb +53 -7
  22. data/lib/googleauth/iam.rb +20 -4
  23. data/lib/googleauth/id_tokens/errors.rb +13 -7
  24. data/lib/googleauth/id_tokens/key_sources.rb +13 -7
  25. data/lib/googleauth/id_tokens/verifier.rb +2 -3
  26. data/lib/googleauth/id_tokens.rb +4 -4
  27. data/lib/googleauth/impersonated_service_account.rb +119 -19
  28. data/lib/googleauth/json_key_reader.rb +17 -3
  29. data/lib/googleauth/oauth2/sts_client.rb +12 -6
  30. data/lib/googleauth/scope_util.rb +2 -2
  31. data/lib/googleauth/service_account.rb +55 -21
  32. data/lib/googleauth/service_account_jwt_header.rb +9 -2
  33. data/lib/googleauth/signet.rb +30 -10
  34. data/lib/googleauth/user_authorizer.rb +38 -10
  35. data/lib/googleauth/user_refresh.rb +54 -19
  36. data/lib/googleauth/version.rb +1 -1
  37. data/lib/googleauth/web_user_authorizer.rb +54 -17
  38. data/lib/googleauth.rb +1 -0
  39. metadata +22 -19
@@ -13,6 +13,8 @@
13
13
  # limitations under the License.
14
14
 
15
15
  require "time"
16
+ require "json"
17
+ require "googleauth/errors"
16
18
  require "googleauth/external_account/base_credentials"
17
19
  require "googleauth/external_account/external_account_utils"
18
20
 
@@ -106,17 +108,32 @@ module Google
106
108
 
107
109
  private
108
110
 
111
+ # Retrieves an IMDSv2 session token or returns a cached token if valid
112
+ #
113
+ # @return [String] The IMDSv2 session token
114
+ # @raise [Google::Auth::CredentialsError] If the token URL is missing or there's an error retrieving the token
109
115
  def imdsv2_session_token
110
116
  return @imdsv2_session_token unless imdsv2_session_token_invalid?
111
- raise "IMDSV2 token url must be provided" if @imdsv2_session_token_url.nil?
117
+ if @imdsv2_session_token_url.nil?
118
+ raise CredentialsError.with_details(
119
+ "IMDSV2 token url must be provided",
120
+ credential_type_name: self.class.name,
121
+ principal: principal
122
+ )
123
+ end
112
124
  begin
113
125
  response = connection.put @imdsv2_session_token_url do |req|
114
126
  req.headers["x-aws-ec2-metadata-token-ttl-seconds"] = IMDSV2_TOKEN_EXPIRATION_IN_SECONDS.to_s
115
127
  end
128
+ raise Faraday::Error unless response.success?
116
129
  rescue Faraday::Error => e
117
- raise "Fetching AWS IMDSV2 token error: #{e}"
130
+ raise CredentialsError.with_details(
131
+ "Fetching AWS IMDSV2 token error: #{e}",
132
+ credential_type_name: self.class.name,
133
+ principal: principal
134
+ )
118
135
  end
119
- raise Faraday::Error unless response.success?
136
+
120
137
  @imdsv2_session_token = response.body
121
138
  @imdsv2_session_token_expiry = Time.now + IMDSV2_TOKEN_EXPIRATION_IN_SECONDS
122
139
  @imdsv2_session_token
@@ -127,6 +144,14 @@ module Google
127
144
  @imdsv2_session_token_expiry.nil? || @imdsv2_session_token_expiry < Time.now
128
145
  end
129
146
 
147
+ # Makes a request to an AWS resource endpoint
148
+ #
149
+ # @param [String] url The AWS endpoint URL
150
+ # @param [String] name Resource name for error messages
151
+ # @param [Hash, nil] data Optional data to send in POST requests
152
+ # @param [Hash] headers Optional request headers
153
+ # @return [Faraday::Response] The successful response
154
+ # @raise [Google::Auth::CredentialsError] If the request fails
130
155
  def get_aws_resource url, name, data: nil, headers: {}
131
156
  begin
132
157
  headers["x-aws-ec2-metadata-token"] = imdsv2_session_token
@@ -136,11 +161,14 @@ module Google
136
161
  else
137
162
  connection.get url, nil, headers
138
163
  end
139
-
140
164
  raise Faraday::Error unless response.success?
141
165
  response
142
166
  rescue Faraday::Error
143
- raise "Failed to retrieve AWS #{name}."
167
+ raise CredentialsError.with_details(
168
+ "Failed to retrieve AWS #{name}.",
169
+ credential_type_name: self.class.name,
170
+ principal: principal
171
+ )
144
172
  end
145
173
  end
146
174
 
@@ -181,9 +209,16 @@ module Google
181
209
  # Retrieves the AWS role currently attached to the current AWS workload by querying the AWS metadata server.
182
210
  # This is needed for the AWS metadata server security credentials endpoint in order to retrieve the AWS security
183
211
  # credentials needed to sign requests to AWS APIs.
212
+ #
213
+ # @return [String] The AWS role name
214
+ # @raise [Google::Auth::CredentialsError] If the credential verification URL is not set or if the request fails
184
215
  def fetch_metadata_role_name
185
216
  unless @credential_verification_url
186
- raise "Unable to determine the AWS metadata server security credentials endpoint"
217
+ raise CredentialsError.with_details(
218
+ "Unable to determine the AWS metadata server security credentials endpoint",
219
+ credential_type_name: self.class.name,
220
+ principal: principal
221
+ )
187
222
  end
188
223
 
189
224
  get_aws_resource(@credential_verification_url, "IAM Role").body
@@ -192,14 +227,25 @@ module Google
192
227
  # Retrieves the AWS security credentials required for signing AWS requests from the AWS metadata server.
193
228
  def fetch_metadata_security_credentials role_name
194
229
  response = get_aws_resource "#{@credential_verification_url}/#{role_name}", "credentials"
195
- MultiJson.load response.body
230
+ JSON.parse response.body
196
231
  end
197
232
 
233
+ # Reads the name of the AWS region from the environment
234
+ #
235
+ # @return [String] The name of the AWS region
236
+ # @raise [Google::Auth::CredentialsError] If the region is not set in the environment
237
+ # and the region_url was not set in credentials source
198
238
  def region
199
239
  @region = ENV[CredentialsLoader::AWS_REGION_VAR] || ENV[CredentialsLoader::AWS_DEFAULT_REGION_VAR]
200
240
 
201
241
  unless @region
202
- raise "region_url or region must be set for external account credentials" unless @region_url
242
+ unless @region_url
243
+ raise CredentialsError.with_details(
244
+ "region_url or region must be set for external account credentials",
245
+ credential_type_name: self.class.name,
246
+ principal: principal
247
+ )
248
+ end
203
249
 
204
250
  @region ||= get_aws_resource(@region_url, "region").body[0..-2]
205
251
  end
@@ -220,23 +266,45 @@ module Google
220
266
  @region_name = region_name
221
267
  end
222
268
 
223
- # Generates the signed request for the provided HTTP request for calling
224
- # an AWS API. This follows the steps described at:
269
+ # Generates an AWS signature version 4 signed request.
270
+ #
271
+ # Creates a signed request following the AWS Signature Version 4 process, which
272
+ # provides secure authentication for AWS API calls. The process includes creating
273
+ # canonical request strings, calculating signatures using the AWS credentials, and
274
+ # building proper authorization headers.
275
+ #
276
+ # For detailed information on the signing process, see:
225
277
  # https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
226
278
  #
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.
279
+ # @param [Hash] aws_credentials The AWS security credentials with the following keys:
280
+ # @option aws_credentials [String] :access_key_id The AWS access key ID
281
+ # @option aws_credentials [String] :secret_access_key The AWS secret access key
282
+ # @option aws_credentials [String, nil] :session_token Optional AWS session token
283
+ # @param [Hash] original_request The request to sign with the following keys:
284
+ # @option original_request [String] :url The AWS service URL (must be HTTPS)
285
+ # @option original_request [String] :method The HTTP method (GET, POST, etc.)
286
+ # @option original_request [Hash, nil] :headers Optional request headers
287
+ # @option original_request [String, nil] :data Optional request payload
288
+ #
289
+ # @return [Hash] The signed request with the following keys:
290
+ # * :url - The original URL as a string
291
+ # * :headers - A hash of headers with the authorization header added
292
+ # * :method - The HTTP method
293
+ # * :data - The request payload (if present)
233
294
  #
234
- # @return [hash{string => string}]
235
- # The AWS signed request dictionary object.
295
+ # @raise [Google::Auth::CredentialsError] If the AWS service URL is invalid
236
296
  #
237
297
  def generate_signed_request aws_credentials, original_request
238
298
  uri = Addressable::URI.parse original_request[:url]
239
- raise "Invalid AWS service URL" unless uri.hostname && uri.scheme == "https"
299
+ unless uri.hostname && uri.scheme == "https"
300
+ # NOTE: We use AwsCredentials name but can't access its principal since AwsRequestSigner
301
+ # is a separate class and not a credential object with access to the audience
302
+ raise CredentialsError.with_details(
303
+ "Invalid AWS service URL",
304
+ credential_type_name: AwsCredentials.name,
305
+ principal: "aws"
306
+ )
307
+ end
240
308
  service_name = uri.host.split(".").first
241
309
 
242
310
  datetime = Time.now.utc.strftime "%Y%m%dT%H%M%SZ"
@@ -12,7 +12,9 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.require "time"
14
14
 
15
+ require "json"
15
16
  require "googleauth/base_client"
17
+ require "googleauth/errors"
16
18
  require "googleauth/helpers/connection"
17
19
  require "googleauth/oauth2/sts_client"
18
20
 
@@ -89,6 +91,14 @@ module Google
89
91
  %r{/iam\.googleapis\.com/locations/[^/]+/workforcePools/}.match?(@audience || "")
90
92
  end
91
93
 
94
+ # For external account credentials, the principal is
95
+ # represented by the audience, such as a workforce pool
96
+ # @private
97
+ # @return [String] the GCP principal, e.g. a workforce pool
98
+ def principal
99
+ @audience
100
+ end
101
+
92
102
  private
93
103
 
94
104
  def token_type
@@ -96,6 +106,8 @@ module Google
96
106
  :access_token
97
107
  end
98
108
 
109
+ # A common method for Other credentials to call during initialization
110
+ # @raise [Google::Auth::InitializationError] If workforce_pool_user_project is incorrectly set
99
111
  def base_setup options
100
112
  self.default_connection = options[:connection]
101
113
 
@@ -121,9 +133,11 @@ module Google
121
133
  connection: default_connection
122
134
  )
123
135
  return unless @workforce_pool_user_project && !is_workforce_pool?
124
- raise "workforce_pool_user_project should not be set for non-workforce pool credentials."
136
+ raise InitializationError, "workforce_pool_user_project should not be set for non-workforce pool credentials."
125
137
  end
126
138
 
139
+ # Exchange tokens at STS endpoint
140
+ # @raise [Google::Auth::AuthorizationError] If the token exchange request fails
127
141
  def exchange_token
128
142
  additional_options = nil
129
143
  if @client_id.nil? && @workforce_pool_user_project
@@ -140,6 +154,12 @@ module Google
140
154
  }
141
155
  log_token_request token_request
142
156
  @sts_client.exchange_token token_request
157
+ rescue Google::Auth::AuthorizationError => e
158
+ raise Google::Auth::AuthorizationError.with_details(
159
+ e.message,
160
+ credential_type_name: self.class.name,
161
+ principal: principal
162
+ )
143
163
  end
144
164
 
145
165
  def log_token_request token_request
@@ -160,19 +180,29 @@ module Google
160
180
  end
161
181
  end
162
182
 
183
+ # Exchanges a token for an impersonated service account access token
184
+ #
185
+ # @param [String] token The token to exchange
186
+ # @param [Hash] _options Additional options (not used)
187
+ # @return [Hash] The response containing the impersonated access token
188
+ # @raise [Google::Auth::CredentialsError] If the impersonation request fails
163
189
  def get_impersonated_access_token token, _options = {}
164
190
  log_impersonated_token_request token
165
191
  response = connection.post @service_account_impersonation_url do |req|
166
192
  req.headers["Authorization"] = "Bearer #{token}"
167
193
  req.headers["Content-Type"] = "application/json"
168
- req.body = MultiJson.dump({ scope: @scope })
194
+ req.body = JSON.generate({ scope: @scope })
169
195
  end
170
196
 
171
197
  if response.status != 200
172
- raise "Service account impersonation failed with status #{response.status}"
198
+ raise CredentialsError.with_details(
199
+ "Service account impersonation failed with status #{response.status}",
200
+ credential_type_name: self.class.name,
201
+ principal: principal
202
+ )
173
203
  end
174
204
 
175
- MultiJson.load response.body
205
+ JSON.parse response.body
176
206
  end
177
207
 
178
208
  def log_impersonated_token_request original_token
@@ -12,7 +12,9 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.require "time"
14
14
 
15
+ require "json"
15
16
  require "googleauth/base_client"
17
+ require "googleauth/errors"
16
18
  require "googleauth/helpers/connection"
17
19
  require "googleauth/oauth2/sts_client"
18
20
 
@@ -36,8 +38,8 @@ module Google
36
38
  # call this API or the required scopes may not be selected:
37
39
  # https://cloud.google.com/resource-manager/reference/rest/v1/projects/get#authorization-scopes
38
40
  #
39
- # @return [string,nil]
40
- # The project ID corresponding to the workload identity pool or workforce pool if determinable.
41
+ # @return [String, nil] The project ID corresponding to the workload identity
42
+ # pool or workforce pool if determinable
41
43
  #
42
44
  def project_id
43
45
  return @project_id unless @project_id.nil?
@@ -53,7 +55,7 @@ module Google
53
55
  end
54
56
 
55
57
  if response.status == 200
56
- response_data = MultiJson.load response.body, symbolize_names: true
58
+ response_data = JSON.parse response.body, symbolize_names: true
57
59
  @project_id = response_data[:projectId]
58
60
  end
59
61
 
@@ -65,7 +67,8 @@ module Google
65
67
  # STS audience pattern:
66
68
  # `//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/...`
67
69
  #
68
- # @return [string, nil]
70
+ # @return [String, nil] The project number extracted from the audience string,
71
+ # or nil if it cannot be determined
69
72
  #
70
73
  def project_number
71
74
  segments = @audience.split "/"
@@ -74,6 +77,11 @@ module Google
74
77
  segments[idx + 1]
75
78
  end
76
79
 
80
+ # Normalizes a timestamp value to a Time object
81
+ #
82
+ # @param time [Time, String, nil] The timestamp to normalize
83
+ # @return [Time, nil] The normalized timestamp or nil if input is nil
84
+ # @raise [Google::Auth::CredentialsError] If the time value is not nil, Time, or String
77
85
  def normalize_timestamp time
78
86
  case time
79
87
  when NilClass
@@ -83,10 +91,14 @@ module Google
83
91
  when String
84
92
  Time.parse time
85
93
  else
86
- raise "Invalid time value #{time}"
94
+ raise CredentialsError, "Invalid time value #{time}"
87
95
  end
88
96
  end
89
97
 
98
+ # Extracts the service account email from the impersonation URL
99
+ #
100
+ # @return [String, nil] The service account email extracted from the
101
+ # service_account_impersonation_url, or nil if it cannot be determined
90
102
  def service_account_email
91
103
  return nil if @service_account_impersonation_url.nil?
92
104
  start_idx = @service_account_impersonation_url.rindex "/"
@@ -13,6 +13,8 @@
13
13
  # limitations under the License.
14
14
 
15
15
  require "time"
16
+ require "json"
17
+ require "googleauth/errors"
16
18
  require "googleauth/external_account/base_credentials"
17
19
  require "googleauth/external_account/external_account_utils"
18
20
 
@@ -32,10 +34,12 @@ module Google
32
34
 
33
35
  # Initialize from options map.
34
36
  #
35
- # @param [string] audience
36
- # @param [hash{symbol => value}] credential_source
37
- # credential_source is a hash that contains either source file or url.
38
- # credential_source_format is either text or json. To define how we parse the credential response.
37
+ # @param [Hash] options Configuration options
38
+ # @option options [String] :audience The audience for the token
39
+ # @option options [Hash{Symbol => Object}] :credential_source A hash containing either source file or url.
40
+ # credential_source_format is either text or json to define how to parse the credential response.
41
+ # @raise [Google::Auth::InitializationError] If credential_source format is invalid, field_name is missing,
42
+ # contains ambiguous sources, or is missing required fields
39
43
  #
40
44
  def initialize options = {}
41
45
  base_setup options
@@ -51,65 +55,87 @@ module Google
51
55
  end
52
56
 
53
57
  # Implementation of BaseCredentials retrieve_subject_token!
58
+ #
59
+ # @return [String] The subject token
60
+ # @raise [Google::Auth::CredentialsError] If the token can't be parsed from JSON or is missing
54
61
  def retrieve_subject_token!
55
62
  content, resource_name = token_data
56
63
  if @credential_source_format_type == "text"
57
64
  token = content
58
65
  else
59
66
  begin
60
- response_data = MultiJson.load content, symbolize_keys: true
67
+ response_data = JSON.parse content, symbolize_names: true
61
68
  token = response_data[@credential_source_field_name.to_sym]
62
69
  rescue StandardError
63
- raise "Unable to parse subject_token from JSON resource #{resource_name} " \
64
- "using key #{@credential_source_field_name}"
70
+ raise CredentialsError, "Unable to parse subject_token from JSON resource #{resource_name} " \
71
+ "using key #{@credential_source_field_name}"
65
72
  end
66
73
  end
67
- raise "Missing subject_token in the credential_source file/response." unless token
74
+ raise CredentialsError, "Missing subject_token in the credential_source file/response." unless token
68
75
  token
69
76
  end
70
77
 
71
78
  private
72
79
 
80
+ # Validates input
81
+ #
82
+ # @raise [Google::Auth::InitializationError] If credential_source format is invalid, field_name is missing,
83
+ # contains ambiguous sources, or is missing required fields
73
84
  def validate_credential_source
74
85
  # `environment_id` is only supported in AWS or dedicated future external account credentials.
75
86
  unless @credential_source[:environment_id].nil?
76
- raise "Invalid Identity Pool credential_source field 'environment_id'"
87
+ raise InitializationError, "Invalid Identity Pool credential_source field 'environment_id'"
77
88
  end
78
89
  unless ["json", "text"].include? @credential_source_format_type
79
- raise "Invalid credential_source format #{@credential_source_format_type}"
90
+ raise InitializationError, "Invalid credential_source format #{@credential_source_format_type}"
80
91
  end
81
92
  # for JSON types, get the required subject_token field name.
82
93
  @credential_source_field_name = @credential_source_format[:subject_token_field_name]
83
94
  if @credential_source_format_type == "json" && @credential_source_field_name.nil?
84
- raise "Missing subject_token_field_name for JSON credential_source format"
95
+ raise InitializationError, "Missing subject_token_field_name for JSON credential_source format"
85
96
  end
86
97
  # check file or url must be fulfilled and mutually exclusiveness.
87
98
  if @credential_source_file && @credential_source_url
88
- raise "Ambiguous credential_source. 'file' is mutually exclusive with 'url'."
99
+ raise InitializationError, "Ambiguous credential_source. 'file' is mutually exclusive with 'url'."
89
100
  end
90
101
  return unless (@credential_source_file || @credential_source_url).nil?
91
- raise "Missing credential_source. A 'file' or 'url' must be provided."
102
+ raise InitializationError, "Missing credential_source. A 'file' or 'url' must be provided."
92
103
  end
93
104
 
94
105
  def token_data
95
106
  @credential_source_file.nil? ? url_data : file_data
96
107
  end
97
108
 
109
+ # Reads data from a file source
110
+ #
111
+ # @return [Array(String, String)] The file content and file path
112
+ # @raise [Google::Auth::CredentialsError] If the source file doesn't exist
98
113
  def file_data
99
- raise "File #{@credential_source_file} was not found." unless File.exist? @credential_source_file
114
+ unless File.exist? @credential_source_file
115
+ raise CredentialsError,
116
+ "File #{@credential_source_file} was not found."
117
+ end
100
118
  content = File.read @credential_source_file, encoding: "utf-8"
101
119
  [content, @credential_source_file]
102
120
  end
103
121
 
122
+ # Fetches data from a URL source
123
+ #
124
+ # @return [Array(String, String)] The response body and URL
125
+ # @raise [Google::Auth::CredentialsError] If there's an error retrieving data from the URL
126
+ # or if the response is not successful
104
127
  def url_data
105
128
  begin
106
129
  response = connection.get @credential_source_url do |req|
107
130
  req.headers.merge! @credential_source_headers
108
131
  end
109
132
  rescue Faraday::Error => e
110
- raise "Error retrieving from credential url: #{e}"
133
+ raise CredentialsError, "Error retrieving from credential url: #{e}"
134
+ end
135
+ unless response.success?
136
+ raise CredentialsError,
137
+ "Unable to retrieve Identity Pool subject token #{response.body}"
111
138
  end
112
- raise "Unable to retrieve Identity Pool subject token #{response.body}" unless response.success?
113
139
  [response.body, @credential_source_url]
114
140
  end
115
141
  end
@@ -14,6 +14,8 @@
14
14
 
15
15
  require "open3"
16
16
  require "time"
17
+ require "json"
18
+ require "googleauth/errors"
17
19
  require "googleauth/external_account/base_credentials"
18
20
  require "googleauth/external_account/external_account_utils"
19
21
 
@@ -41,34 +43,43 @@ module Google
41
43
 
42
44
  # Initialize from options map.
43
45
  #
44
- # @param [string] audience
45
- # @param [hash{symbol => value}] credential_source
46
- # credential_source is a hash that contains either source file or url.
47
- # credential_source_format is either text or json. To define how we parse the credential response.
48
- #
46
+ # @param [Hash] options Configuration options
47
+ # @option options [String] :audience Audience for the token
48
+ # @option options [Hash] :credential_source Credential source configuration that contains executable
49
+ # configuration
50
+ # @raise [Google::Auth::InitializationError] If executable source, command is missing, or timeout is invalid
49
51
  def initialize options = {}
50
52
  base_setup options
51
53
 
52
54
  @audience = options[:audience]
53
55
  @credential_source = options[:credential_source] || {}
54
56
  @credential_source_executable = @credential_source[:executable]
55
- raise "Missing excutable source. An 'executable' must be provided" if @credential_source_executable.nil?
57
+ if @credential_source_executable.nil?
58
+ raise InitializationError,
59
+ "Missing excutable source. An 'executable' must be provided"
60
+ end
56
61
  @credential_source_executable_command = @credential_source_executable[:command]
57
62
  if @credential_source_executable_command.nil?
58
- raise "Missing command field. Executable command must be provided."
63
+ raise InitializationError, "Missing command field. Executable command must be provided."
59
64
  end
60
65
  @credential_source_executable_timeout_millis = @credential_source_executable[:timeout_millis] ||
61
66
  EXECUTABLE_TIMEOUT_MILLIS_DEFAULT
62
67
  if @credential_source_executable_timeout_millis < EXECUTABLE_TIMEOUT_MILLIS_LOWER_BOUND ||
63
68
  @credential_source_executable_timeout_millis > EXECUTABLE_TIMEOUT_MILLIS_UPPER_BOUND
64
- raise "Timeout must be between 5 and 120 seconds."
69
+ raise InitializationError, "Timeout must be between 5 and 120 seconds."
65
70
  end
66
71
  @credential_source_executable_output_file = @credential_source_executable[:output_file]
67
72
  end
68
73
 
74
+ # Retrieves the subject token using the credential_source object.
75
+ #
76
+ # @return [String] The retrieved subject token
77
+ # @raise [Google::Auth::CredentialsError] If executables are not allowed, if token retrieval fails,
78
+ # or if the token is invalid
69
79
  def retrieve_subject_token!
70
80
  unless ENV[ENABLE_PLUGGABLE_ENV] == "1"
71
- raise "Executables need to be explicitly allowed (set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES to '1') " \
81
+ raise CredentialsError,
82
+ "Executables need to be explicitly allowed (set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES to '1') " \
72
83
  "to run."
73
84
  end
74
85
  # check output file first
@@ -78,7 +89,7 @@ module Google
78
89
  env = inject_environment_variables
79
90
  output = subprocess_with_timeout env, @credential_source_executable_command,
80
91
  @credential_source_executable_timeout_millis
81
- response = MultiJson.load output, symbolize_keys: true
92
+ response = JSON.parse output, symbolize_names: true
82
93
  parse_subject_token response
83
94
  end
84
95
 
@@ -89,7 +100,7 @@ module Google
89
100
  return nil unless File.exist? @credential_source_executable_output_file
90
101
  begin
91
102
  content = File.read @credential_source_executable_output_file, encoding: "utf-8"
92
- response = MultiJson.load content, symbolize_keys: true
103
+ response = JSON.parse content, symbolize_names: true
93
104
  rescue StandardError
94
105
  return nil
95
106
  end
@@ -97,7 +108,7 @@ module Google
97
108
  subject_token = parse_subject_token response
98
109
  rescue StandardError => e
99
110
  return nil if e.message.match(/The token returned by the executable is expired/)
100
- raise e
111
+ raise CredentialsError, e.message
101
112
  end
102
113
  subject_token
103
114
  end
@@ -106,25 +117,29 @@ module Google
106
117
  validate_response_schema response
107
118
  unless response[:success]
108
119
  if response[:code].nil? || response[:message].nil?
109
- raise "Error code and message fields are required in the response."
120
+ raise CredentialsError, "Error code and message fields are required in the response."
110
121
  end
111
- raise "Executable returned unsuccessful response: code: #{response[:code]}, message: #{response[:message]}."
122
+ raise CredentialsError,
123
+ "Executable returned unsuccessful response: code: #{response[:code]}, message: #{response[:message]}."
112
124
  end
113
125
  if response[:expiration_time] && response[:expiration_time] < Time.now.to_i
114
- raise "The token returned by the executable is expired."
126
+ raise CredentialsError, "The token returned by the executable is expired."
127
+ end
128
+ if response[:token_type].nil?
129
+ raise CredentialsError,
130
+ "The executable response is missing the token_type field."
115
131
  end
116
- raise "The executable response is missing the token_type field." if response[:token_type].nil?
117
132
  return response[:id_token] if ID_TOKEN_TYPE.include? response[:token_type]
118
133
  return response[:saml_response] if response[:token_type] == "urn:ietf:params:oauth:token-type:saml2"
119
- raise "Executable returned unsupported token type."
134
+ raise CredentialsError, "Executable returned unsupported token type."
120
135
  end
121
136
 
122
137
  def validate_response_schema response
123
- raise "The executable response is missing the version field." if response[:version].nil?
138
+ raise CredentialsError, "The executable response is missing the version field." if response[:version].nil?
124
139
  if response[:version] > EXECUTABLE_SUPPORTED_MAX_VERSION
125
- raise "Executable returned unsupported version #{response[:version]}."
140
+ raise CredentialsError, "Executable returned unsupported version #{response[:version]}."
126
141
  end
127
- raise "The executable response is missing the success field." if response[:success].nil?
142
+ raise CredentialsError, "The executable response is missing the success field." if response[:success].nil?
128
143
  end
129
144
 
130
145
  def inject_environment_variables
@@ -145,7 +160,8 @@ module Google
145
160
  Timeout.timeout timeout_seconds do
146
161
  output, error, status = Open3.capture3 environment_vars, command
147
162
  unless status.success?
148
- raise "Executable exited with non-zero return code #{status.exitstatus}. Error: #{output}, #{error}"
163
+ raise CredentialsError,
164
+ "Executable exited with non-zero return code #{status.exitstatus}. Error: #{output}, #{error}"
149
165
  end
150
166
  output
151
167
  end