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 "googleauth/errors"
16
+
15
17
  module Google
16
18
  # Module Auth provides classes that provide Google-specific authorization
17
19
  # used to access Google APIs.
@@ -19,15 +21,23 @@ module Google
19
21
  # JsonKeyReader contains the behaviour used to read private key and
20
22
  # client email fields from the service account
21
23
  module JsonKeyReader
24
+ # Reads a JSON key from an IO object and extracts common fields.
25
+ #
26
+ # @param json_key_io [IO] An IO object containing the JSON key
27
+ # @return [Array(String, String, String, String, String)] An array containing:
28
+ # private_key, client_email, project_id, quota_project_id, and universe_domain
29
+ # @raise [Google::Auth::InitializationError] If client_email or private_key
30
+ # fields are missing from the JSON
22
31
  def read_json_key json_key_io
23
32
  json_key = MultiJson.load json_key_io.read
24
- raise "missing client_email" unless json_key.key? "client_email"
25
- raise "missing private_key" unless json_key.key? "private_key"
33
+ raise InitializationError, "missing client_email" unless json_key.key? "client_email"
34
+ raise InitializationError, "missing private_key" unless json_key.key? "private_key"
26
35
  [
27
36
  json_key["private_key"],
28
37
  json_key["client_email"],
29
38
  json_key["project_id"],
30
- json_key["quota_project_id"]
39
+ json_key["quota_project_id"],
40
+ json_key["universe_domain"]
31
41
  ]
32
42
  end
33
43
  end
@@ -12,6 +12,7 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ require "googleauth/errors"
15
16
  require "googleauth/helpers/connection"
16
17
 
17
18
  module Google
@@ -36,10 +37,12 @@ module Google
36
37
 
37
38
  # Create a new instance of the STSClient.
38
39
  #
39
- # @param [String] token_exchange_endpoint
40
- # The token exchange endpoint.
40
+ # @param [Hash] options Configuration options
41
+ # @option options [String] :token_exchange_endpoint The token exchange endpoint
42
+ # @option options [Faraday::Connection] :connection The Faraday connection to use
43
+ # @raise [Google::Auth::InitializationError] If token_exchange_endpoint is nil
41
44
  def initialize options = {}
42
- raise "Token exchange endpoint can not be nil" if options[:token_exchange_endpoint].nil?
45
+ raise InitializationError, "Token exchange endpoint can not be nil" if options[:token_exchange_endpoint].nil?
43
46
  self.default_connection = options[:connection]
44
47
  @token_exchange_endpoint = options[:token_exchange_endpoint]
45
48
  end
@@ -67,6 +70,8 @@ module Google
67
70
  # The optional additional headers to pass to the token exchange endpoint.
68
71
  #
69
72
  # @return [Hash] A hash containing the token exchange response.
73
+ # @raise [ArgumentError] If required options are missing
74
+ # @raise [Google::Auth::AuthorizationError] If the token exchange request fails
70
75
  def exchange_token options = {}
71
76
  missing_required_opts = [:grant_type, :subject_token, :subject_token_type] - options.keys
72
77
  unless missing_required_opts.empty?
@@ -81,7 +86,7 @@ module Google
81
86
  response = connection.post @token_exchange_endpoint, URI.encode_www_form(request_body), headers
82
87
 
83
88
  if response.status != 200
84
- raise "Token exchange failed with status #{response.status}"
89
+ raise AuthorizationError, "Token exchange failed with status #{response.status}"
85
90
  end
86
91
 
87
92
  MultiJson.load response.body
@@ -57,7 +57,7 @@ module Google
57
57
  #
58
58
  # @param scope [String,Array<String>] Input scope(s)
59
59
  # @return [Array<String>] Always an array of strings
60
- # @raise ArgumentError If the input is not a string or array of strings
60
+ # @raise [ArgumentError] If the input is not a string or array of strings
61
61
  #
62
62
  def self.as_array scope
63
63
  case scope
@@ -12,13 +12,16 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- require "googleauth/signet"
16
- require "googleauth/credentials_loader"
17
- require "googleauth/json_key_reader"
18
15
  require "jwt"
19
16
  require "multi_json"
20
17
  require "stringio"
21
18
 
19
+ require "google/logging/message"
20
+ require "googleauth/signet"
21
+ require "googleauth/credentials_loader"
22
+ require "googleauth/json_key_reader"
23
+ require "googleauth/service_account_jwt_header"
24
+
22
25
  module Google
23
26
  # Module Auth provides classes that provide Google-specific authorization
24
27
  # used to access Google APIs.
@@ -39,13 +42,18 @@ module Google
39
42
  attr_reader :quota_project_id
40
43
 
41
44
  def enable_self_signed_jwt?
42
- @enable_self_signed_jwt
45
+ # Use a self-singed JWT if there's no information that can be used to
46
+ # obtain an OAuth token, OR if there are scopes but also an assertion
47
+ # that they are default scopes that shouldn't be used to fetch a token,
48
+ # OR we are not in the default universe and thus OAuth isn't supported.
49
+ target_audience.nil? && (scope.nil? || @enable_self_signed_jwt || universe_domain != "googleapis.com")
43
50
  end
44
51
 
45
52
  # Creates a ServiceAccountCredentials.
46
53
  #
47
- # @param json_key_io [IO] an IO from which the JSON key can be read
54
+ # @param json_key_io [IO] An IO object containing the JSON key
48
55
  # @param scope [string|array|nil] the scope(s) to access
56
+ # @raise [ArgumentError] If both scope and target_audience are specified
49
57
  def self.make_creds options = {}
50
58
  json_key_io, scope, enable_self_signed_jwt, target_audience, audience, token_credential_uri =
51
59
  options.values_at :json_key_io, :scope, :enable_self_signed_jwt, :target_audience,
@@ -53,12 +61,13 @@ module Google
53
61
  raise ArgumentError, "Cannot specify both scope and target_audience" if scope && target_audience
54
62
 
55
63
  if json_key_io
56
- private_key, client_email, project_id, quota_project_id = read_json_key json_key_io
64
+ private_key, client_email, project_id, quota_project_id, universe_domain = read_json_key json_key_io
57
65
  else
58
66
  private_key = unescape ENV[CredentialsLoader::PRIVATE_KEY_VAR]
59
67
  client_email = ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
60
68
  project_id = ENV[CredentialsLoader::PROJECT_ID_VAR]
61
69
  quota_project_id = nil
70
+ universe_domain = nil
62
71
  end
63
72
  project_id ||= CredentialsLoader.load_gcloud_project_id
64
73
 
@@ -70,13 +79,41 @@ module Google
70
79
  issuer: client_email,
71
80
  signing_key: OpenSSL::PKey::RSA.new(private_key),
72
81
  project_id: project_id,
73
- quota_project_id: quota_project_id)
82
+ quota_project_id: quota_project_id,
83
+ universe_domain: universe_domain || "googleapis.com")
74
84
  .configure_connection(options)
75
85
  end
76
86
 
87
+ # Creates a duplicate of these credentials
88
+ # without the Signet::OAuth2::Client-specific
89
+ # transient state (e.g. cached tokens)
90
+ #
91
+ # @param options [Hash] Overrides for the credentials parameters.
92
+ # The following keys are recognized in addition to keys in the
93
+ # Signet::OAuth2::Client
94
+ # * `:enable_self_signed_jwt` Whether the self-signed JWT should
95
+ # be used for the authentication
96
+ # * `project_id` the project id to use during the authentication
97
+ # * `quota_project_id` the quota project id to use
98
+ # during the authentication
99
+ def duplicate options = {}
100
+ options = deep_hash_normalize options
101
+ super(
102
+ {
103
+ enable_self_signed_jwt: @enable_self_signed_jwt,
104
+ project_id: project_id,
105
+ quota_project_id: quota_project_id,
106
+ logger: logger
107
+ }.merge(options)
108
+ )
109
+ end
110
+
77
111
  # Handles certain escape sequences that sometimes appear in input.
78
112
  # Specifically, interprets the "\n" sequence for newline, and removes
79
113
  # enclosing quotes.
114
+ #
115
+ # @param str [String] The string to unescape
116
+ # @return [String] The unescaped string
80
117
  def self.unescape str
81
118
  str = str.gsub '\n', "\n"
82
119
  str = str[1..-2] if str.start_with?('"') && str.end_with?('"')
@@ -93,16 +130,51 @@ module Google
93
130
  # Extends the base class to use a transient
94
131
  # ServiceAccountJwtHeaderCredentials for certain cases.
95
132
  def apply! a_hash, opts = {}
96
- # Use a self-singed JWT if there's no information that can be used to
97
- # obtain an OAuth token, OR if there are scopes but also an assertion
98
- # that they are default scopes that shouldn't be used to fetch a token.
99
- if target_audience.nil? && (scope.nil? || enable_self_signed_jwt?)
133
+ if enable_self_signed_jwt?
100
134
  apply_self_signed_jwt! a_hash
101
135
  else
102
136
  super
103
137
  end
104
138
  end
105
139
 
140
+ # Modifies this logic so it also requires self-signed-jwt to be disabled
141
+ def needs_access_token?
142
+ super && !enable_self_signed_jwt?
143
+ end
144
+
145
+ # Destructively updates these credentials
146
+ #
147
+ # This method is called by `Signet::OAuth2::Client`'s constructor
148
+ #
149
+ # @param options [Hash] Overrides for the credentials parameters.
150
+ # The following keys are recognized in addition to keys in the
151
+ # Signet::OAuth2::Client
152
+ # * `:enable_self_signed_jwt` Whether the self-signed JWT should
153
+ # be used for the authentication
154
+ # * `project_id` the project id to use during the authentication
155
+ # * `quota_project_id` the quota project id to use
156
+ # during the authentication
157
+ # @return [Google::Auth::ServiceAccountCredentials]
158
+ def update! options = {}
159
+ # Normalize all keys to symbols to allow indifferent access.
160
+ options = deep_hash_normalize options
161
+
162
+ @enable_self_signed_jwt = options[:enable_self_signed_jwt] ? true : false
163
+ @project_id = options[:project_id] if options.key? :project_id
164
+ @quota_project_id = options[:quota_project_id] if options.key? :quota_project_id
165
+
166
+ super(options)
167
+
168
+ self
169
+ end
170
+
171
+ # Returns the client email as the principal for service account credentials
172
+ # @private
173
+ # @return [String] the email address of the service account
174
+ def principal
175
+ @issuer
176
+ end
177
+
106
178
  private
107
179
 
108
180
  def apply_self_signed_jwt! a_hash
@@ -115,101 +187,9 @@ module Google
115
187
  }
116
188
  key_io = StringIO.new MultiJson.dump(cred_json)
117
189
  alt = ServiceAccountJwtHeaderCredentials.make_creds json_key_io: key_io, scope: scope
190
+ alt.logger = logger
118
191
  alt.apply! a_hash
119
192
  end
120
193
  end
121
-
122
- # Authenticates requests using Google's Service Account credentials via
123
- # JWT Header.
124
- #
125
- # This class allows authorizing requests for service accounts directly
126
- # from credentials from a json key file downloaded from the developer
127
- # console (via 'Generate new Json Key'). It is not part of any OAuth2
128
- # flow, rather it creates a JWT and sends that as a credential.
129
- #
130
- # cf [Application Default Credentials](https://cloud.google.com/docs/authentication/production)
131
- class ServiceAccountJwtHeaderCredentials
132
- JWT_AUD_URI_KEY = :jwt_aud_uri
133
- AUTH_METADATA_KEY = Google::Auth::BaseClient::AUTH_METADATA_KEY
134
- TOKEN_CRED_URI = "https://www.googleapis.com/oauth2/v4/token".freeze
135
- SIGNING_ALGORITHM = "RS256".freeze
136
- EXPIRY = 60
137
- extend CredentialsLoader
138
- extend JsonKeyReader
139
- attr_reader :project_id
140
- attr_reader :quota_project_id
141
-
142
- # Create a ServiceAccountJwtHeaderCredentials.
143
- #
144
- # @param json_key_io [IO] an IO from which the JSON key can be read
145
- # @param scope [string|array|nil] the scope(s) to access
146
- def self.make_creds options = {}
147
- json_key_io, scope = options.values_at :json_key_io, :scope
148
- new json_key_io: json_key_io, scope: scope
149
- end
150
-
151
- # Initializes a ServiceAccountJwtHeaderCredentials.
152
- #
153
- # @param json_key_io [IO] an IO from which the JSON key can be read
154
- def initialize options = {}
155
- json_key_io = options[:json_key_io]
156
- if json_key_io
157
- @private_key, @issuer, @project_id, @quota_project_id =
158
- self.class.read_json_key json_key_io
159
- else
160
- @private_key = ENV[CredentialsLoader::PRIVATE_KEY_VAR]
161
- @issuer = ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
162
- @project_id = ENV[CredentialsLoader::PROJECT_ID_VAR]
163
- @quota_project_id = nil
164
- end
165
- @project_id ||= CredentialsLoader.load_gcloud_project_id
166
- @signing_key = OpenSSL::PKey::RSA.new @private_key
167
- @scope = options[:scope]
168
- end
169
-
170
- # Construct a jwt token if the JWT_AUD_URI key is present in the input
171
- # hash.
172
- #
173
- # The jwt token is used as the value of a 'Bearer '.
174
- def apply! a_hash, opts = {}
175
- jwt_aud_uri = a_hash.delete JWT_AUD_URI_KEY
176
- return a_hash if jwt_aud_uri.nil? && @scope.nil?
177
- jwt_token = new_jwt_token jwt_aud_uri, opts
178
- a_hash[AUTH_METADATA_KEY] = "Bearer #{jwt_token}"
179
- a_hash
180
- end
181
-
182
- # Returns a clone of a_hash updated with the authoriation header
183
- def apply a_hash, opts = {}
184
- a_copy = a_hash.clone
185
- apply! a_copy, opts
186
- a_copy
187
- end
188
-
189
- # Returns a reference to the #apply method, suitable for passing as
190
- # a closure
191
- def updater_proc
192
- proc { |a_hash, opts = {}| apply a_hash, opts }
193
- end
194
-
195
- # Creates a jwt uri token.
196
- def new_jwt_token jwt_aud_uri = nil, options = {}
197
- now = Time.new
198
- skew = options[:skew] || 60
199
- assertion = {
200
- "iss" => @issuer,
201
- "sub" => @issuer,
202
- "exp" => (now + EXPIRY).to_i,
203
- "iat" => (now - skew).to_i
204
- }
205
-
206
- jwt_aud_uri = nil if @scope
207
-
208
- assertion["scope"] = Array(@scope).join " " if @scope
209
- assertion["aud"] = jwt_aud_uri if jwt_aud_uri
210
-
211
- JWT.encode assertion, @signing_key, SIGNING_ALGORITHM
212
- end
213
- end
214
194
  end
215
195
  end
@@ -0,0 +1,187 @@
1
+ # Copyright 2025 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 "google/logging/message"
16
+ require "googleauth/credentials_loader"
17
+ require "googleauth/json_key_reader"
18
+ require "jwt"
19
+
20
+ module Google
21
+ # Module Auth provides classes that provide Google-specific authorization
22
+ # used to access Google APIs.
23
+ module Auth
24
+ # Authenticates requests using Google's Service Account credentials via
25
+ # JWT Header.
26
+ #
27
+ # This class allows authorizing requests for service accounts directly
28
+ # from credentials from a json key file downloaded from the developer
29
+ # console (via 'Generate new Json Key'). It is not part of any OAuth2
30
+ # flow, rather it creates a JWT and sends that as a credential.
31
+ #
32
+ # cf [Application Default Credentials](https://cloud.google.com/docs/authentication/production)
33
+ class ServiceAccountJwtHeaderCredentials
34
+ JWT_AUD_URI_KEY = :jwt_aud_uri
35
+ AUTH_METADATA_KEY = Google::Auth::BaseClient::AUTH_METADATA_KEY
36
+ TOKEN_CRED_URI = "https://www.googleapis.com/oauth2/v4/token".freeze
37
+ SIGNING_ALGORITHM = "RS256".freeze
38
+ EXPIRY = 60
39
+
40
+ extend CredentialsLoader
41
+ extend JsonKeyReader
42
+
43
+ attr_reader :project_id
44
+ attr_reader :quota_project_id
45
+ attr_accessor :universe_domain
46
+ attr_accessor :logger
47
+
48
+ # Create a ServiceAccountJwtHeaderCredentials.
49
+ #
50
+ # @param json_key_io [IO] An IO object containing the JSON key
51
+ # @param scope [string|array|nil] the scope(s) to access
52
+ def self.make_creds options = {}
53
+ json_key_io, scope = options.values_at :json_key_io, :scope
54
+ new json_key_io: json_key_io, scope: scope
55
+ end
56
+
57
+ # Initializes a ServiceAccountJwtHeaderCredentials.
58
+ #
59
+ # @param json_key_io [IO] An IO object containing the JSON key
60
+ def initialize options = {}
61
+ json_key_io = options[:json_key_io]
62
+ if json_key_io
63
+ @private_key, @issuer, @project_id, @quota_project_id, @universe_domain =
64
+ self.class.read_json_key json_key_io
65
+ else
66
+ @private_key = options.key?(:private_key) ? options[:private_key] : ENV[CredentialsLoader::PRIVATE_KEY_VAR]
67
+ @issuer = options.key?(:issuer) ? options[:issuer] : ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
68
+ @project_id = options.key?(:project_id) ? options[:project_id] : ENV[CredentialsLoader::PROJECT_ID_VAR]
69
+ @quota_project_id = options[:quota_project_id] if options.key? :quota_project_id
70
+ @universe_domain = options[:universe_domain] if options.key? :universe_domain
71
+ end
72
+ @universe_domain ||= "googleapis.com"
73
+ @project_id ||= CredentialsLoader.load_gcloud_project_id
74
+ @signing_key = OpenSSL::PKey::RSA.new @private_key
75
+ @scope = options[:scope] if options.key? :scope
76
+ @logger = options[:logger] if options.key? :logger
77
+ end
78
+
79
+ # Creates a duplicate of these credentials
80
+ #
81
+ # @param options [Hash] Overrides for the credentials parameters.
82
+ # The following keys are recognized
83
+ # * `private key` the private key in string form
84
+ # * `issuer` the SA issuer
85
+ # * `scope` the scope(s) to access
86
+ # * `project_id` the project id to use during the authentication
87
+ # * `quota_project_id` the quota project id to use
88
+ # * `universe_domain` the universe domain of the credentials
89
+ def duplicate options = {}
90
+ options = deep_hash_normalize options
91
+
92
+ options = {
93
+ private_key: @private_key,
94
+ issuer: @issuer,
95
+ scope: @scope,
96
+ project_id: project_id,
97
+ quota_project_id: quota_project_id,
98
+ universe_domain: universe_domain,
99
+ logger: logger
100
+ }.merge(options)
101
+
102
+ self.class.new options
103
+ end
104
+
105
+ # Construct a jwt token if the JWT_AUD_URI key is present in the input
106
+ # hash.
107
+ #
108
+ # The jwt token is used as the value of a 'Bearer '.
109
+ def apply! a_hash, opts = {}
110
+ jwt_aud_uri = a_hash.delete JWT_AUD_URI_KEY
111
+ return a_hash if jwt_aud_uri.nil? && @scope.nil?
112
+ jwt_token = new_jwt_token jwt_aud_uri, opts
113
+ a_hash[AUTH_METADATA_KEY] = "Bearer #{jwt_token}"
114
+ logger&.debug do
115
+ hash = Digest::SHA256.hexdigest jwt_token
116
+ Google::Logging::Message.from message: "Sending JWT auth token. (sha256:#{hash})"
117
+ end
118
+ a_hash
119
+ end
120
+
121
+ # Returns a clone of a_hash updated with the authorization header
122
+ def apply a_hash, opts = {}
123
+ a_copy = a_hash.clone
124
+ apply! a_copy, opts
125
+ a_copy
126
+ end
127
+
128
+ # Returns a reference to the #apply method, suitable for passing as
129
+ # a closure
130
+ def updater_proc
131
+ proc { |a_hash, opts = {}| apply a_hash, opts }
132
+ end
133
+
134
+ # Creates a jwt uri token.
135
+ def new_jwt_token jwt_aud_uri = nil, options = {}
136
+ now = Time.new
137
+ skew = options[:skew] || 60
138
+ assertion = {
139
+ "iss" => @issuer,
140
+ "sub" => @issuer,
141
+ "exp" => (now + EXPIRY).to_i,
142
+ "iat" => (now - skew).to_i
143
+ }
144
+
145
+ jwt_aud_uri = nil if @scope
146
+
147
+ assertion["scope"] = Array(@scope).join " " if @scope
148
+ assertion["aud"] = jwt_aud_uri if jwt_aud_uri
149
+
150
+ logger&.debug do
151
+ Google::Logging::Message.from message: "JWT assertion: #{assertion}"
152
+ end
153
+
154
+ JWT.encode assertion, @signing_key, SIGNING_ALGORITHM
155
+ end
156
+
157
+ # Duck-types the corresponding method from BaseClient
158
+ def needs_access_token?
159
+ false
160
+ end
161
+
162
+ # Returns the client email as the principal for service account JWT header credentials
163
+ # @private
164
+ # @return [String] the email address of the service account
165
+ def principal
166
+ @issuer
167
+ end
168
+
169
+ private
170
+
171
+ def deep_hash_normalize old_hash
172
+ sym_hash = {}
173
+ old_hash&.each { |k, v| sym_hash[k.to_sym] = recursive_hash_normalize_keys v }
174
+ sym_hash
175
+ end
176
+
177
+ # Convert all keys in this hash (nested) to symbols for uniform retrieval
178
+ def recursive_hash_normalize_keys val
179
+ if val.is_a? Hash
180
+ deep_hash_normalize val
181
+ else
182
+ val
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end