Skip to content

Commit 47ae816

Browse files
authored
fix: Deprecate method make_creds in DefaultCredentials (#545)
1 parent 09ac290 commit 47ae816

File tree

11 files changed

+225
-95
lines changed

11 files changed

+225
-95
lines changed

Credentials.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,19 @@ that exposes common initialization functionality, such as creating credentials f
3838
- For obtaining authentication tokens from GCE metadata server
3939
- Used automatically when code is running on Google Compute Engine
4040
- Fetches tokens from the metadata server with no additional configuration needed
41+
- This credential type does not have a supported JSON form
4142

4243
4. **Google::Auth::IAMCredentials < Signet::OAuth2::Client** - `lib/googleauth/iam.rb`
4344
- For IAM-based authentication (e.g. service-to-service)
4445
- Implements authentication-as-a-service for systems already authenticated
4546
- Exchanges existing credentials for a short-lived access token
47+
- This credential type does not have a supported JSON form
4648

4749
## Service Account Authentication
4850

4951
5. **Google::Auth::ServiceAccountCredentials < Signet::OAuth2::Client** - `lib/googleauth/service_account.rb`
5052
- Authenticates requests using Service Account credentials via an OAuth access token
51-
- Created from JSON key file downloaded from Google Cloud Console
53+
- Created from JSON key file downloaded from Google Cloud Console. The JSON form of this credential type has a `"type"` field with the value `"service_account"`.
5254
- Supports both OAuth access tokens and self-signed JWT authentication
5355
- Can specify scopes for access token requests
5456

@@ -64,14 +66,15 @@ that exposes common initialization functionality, such as creating credentials f
6466
- Allows a GCP principal identified by a set of source credentials to impersonate a service account
6567
- Useful for delegation of authority and managing permissions across service accounts
6668
- Source credentials must have the Service Account Token Creator role on the target
69+
- This credential type does not have a supported JSON form
6770

6871
## User Authentication
6972

7073
8. **Google::Auth::UserRefreshCredentials < Signet::OAuth2::Client** - `lib/googleauth/user_refresh.rb`
7174
- For user refresh token authentication (from 3-legged OAuth flow)
7275
- Authenticates on behalf of a user who has authorized the application
7376
- Handles token refresh when original access token expires
74-
- Typically obtained through web or installed application flow
77+
- Typically obtained through web or installed application flow. The JSON form of this credential type has a `"type"` field with the value `"authorized_user"`.
7578

7679
`Google::Auth::UserAuthorizer` (`lib/googleauth/user_authorizer.rb`) and `Google::Auth::WebUserAuthorizer` (`lib/googleauth/web_user_authorizer.rb`)
7780
are used to facilitate user authentication. The `UserAuthorizer` handles interactive 3-Legged-OAuth2 (3LO) user consent authorization for command-line applications.
@@ -83,6 +86,7 @@ that exposes common initialization functionality, such as creating credentials f
8386
types based on credential source (similar to `Google::Auth::get_application_default`).
8487
It is included in all External Account credentials types, and it itself includes `Google::Auth::BaseClient` module so all External
8588
Account credentials types include `Google::Auth::BaseClient`.
89+
The JSON form of this credential type has a `"type"` field with the value `"external_account"`.
8690

8791
9. **Google::Auth::ExternalAccount::AwsCredentials** - `lib/googleauth/external_account/aws_credentials.rb`
8892
- Includes `Google::Auth::BaseClient` module

lib/googleauth/credentials.rb

Lines changed: 49 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
require "json"
1717
require "pathname"
1818
require "signet/oauth_2/client"
19+
require "multi_json"
1920

2021
require "googleauth/credentials_loader"
2122
require "googleauth/errors"
@@ -508,17 +509,61 @@ def self.from_io io, options
508509
token_credential_uri: options[:token_credential_uri] || token_credential_uri,
509510
audience: options[:audience] || audience
510511
}
511-
client = Google::Auth::DefaultCredentials.make_creds creds_input
512+
513+
# Determine the class, which consumes the IO stream
514+
json_key, clz = Google::Auth::DefaultCredentials.determine_creds_class creds_input[:json_key_io]
515+
516+
# Re-serialize the parsed JSON and replace the IO stream in creds_input
517+
creds_input[:json_key_io] = StringIO.new MultiJson.dump(json_key)
518+
519+
client = clz.make_creds creds_input
512520
options = options.select { |k, _v| k == :logger }
513521
new client, options
514522
end
515523

524+
# @private
525+
# Initializes the Signet client.
526+
def self.init_client hash, options = {}
527+
options = update_client_options options
528+
io = StringIO.new JSON.generate hash
529+
530+
# Determine the class, which consumes the IO stream
531+
json_key, clz = Google::Auth::DefaultCredentials.determine_creds_class io
532+
533+
# Re-serialize the parsed JSON and create a new IO stream.
534+
new_io = StringIO.new MultiJson.dump(json_key)
535+
536+
clz.make_creds options.merge!(json_key_io: new_io)
537+
end
538+
539+
# @private
540+
# Updates client options with defaults from the credential class
541+
#
542+
# @param [Hash] options Options to update
543+
# @return [Hash] Updated options hash
544+
# @raise [ArgumentError] If both scope and target_audience are specified
545+
def self.update_client_options options
546+
options = options.dup
547+
548+
# options have higher priority over constructor defaults
549+
options[:token_credential_uri] ||= token_credential_uri
550+
options[:audience] ||= audience
551+
options[:scope] ||= scope
552+
options[:target_audience] ||= target_audience
553+
554+
if !Array(options[:scope]).empty? && options[:target_audience]
555+
raise ArgumentError, "Cannot specify both scope and target_audience"
556+
end
557+
options.delete :scope unless options[:target_audience].nil?
558+
559+
options
560+
end
561+
516562
private_class_method :from_env_vars,
517563
:from_default_paths,
518564
:from_application_default,
519565
:from_io
520566

521-
522567
# Creates a duplicate of these credentials. This method tries to create the duplicate of the
523568
# wrapped credentials if they support duplication and use them as is if they don't.
524569
#
@@ -571,14 +616,6 @@ def verify_keyfile_exists! keyfile
571616
raise InitializationError, "The keyfile '#{keyfile}' is not a valid file." unless exists
572617
end
573618

574-
# Initializes the Signet client.
575-
def init_client hash, options = {}
576-
options = update_client_options options
577-
io = StringIO.new JSON.generate hash
578-
options.merge! json_key_io: io
579-
Google::Auth::DefaultCredentials.make_creds options
580-
end
581-
582619
# returns a new Hash with string keys instead of symbol keys.
583620
def stringify_hash_keys hash
584621
hash.to_h.transform_keys(&:to_s)
@@ -589,28 +626,6 @@ def symbolize_hash_keys hash
589626
hash.to_h.transform_keys(&:to_sym)
590627
end
591628

592-
# Updates client options with defaults from the credential class
593-
#
594-
# @param [Hash] options Options to update
595-
# @return [Hash] Updated options hash
596-
# @raise [ArgumentError] If both scope and target_audience are specified
597-
def update_client_options options
598-
options = options.dup
599-
600-
# options have higher priority over constructor defaults
601-
options[:token_credential_uri] ||= self.class.token_credential_uri
602-
options[:audience] ||= self.class.audience
603-
options[:scope] ||= self.class.scope
604-
options[:target_audience] ||= self.class.target_audience
605-
606-
if !Array(options[:scope]).empty? && options[:target_audience]
607-
raise ArgumentError, "Cannot specify both scope and target_audience"
608-
end
609-
options.delete :scope unless options[:target_audience].nil?
610-
611-
options
612-
end
613-
614629
def update_from_client client
615630
@project_id ||= client.project_id if client.respond_to? :project_id
616631
@quota_project_id ||= client.quota_project_id if client.respond_to? :quota_project_id
@@ -624,7 +639,7 @@ def update_from_hash hash, options
624639
hash["target_audience"] ||= options[:target_audience]
625640
@project_id ||= hash["project_id"] || hash["project"]
626641
@quota_project_id ||= hash["quota_project_id"]
627-
@client = init_client hash, options
642+
@client = self.class.init_client hash, options
628643
end
629644

630645
def update_from_filepath path, options
@@ -634,7 +649,7 @@ def update_from_filepath path, options
634649
json["target_audience"] ||= options[:target_audience]
635650
@project_id ||= json["project_id"] || json["project"]
636651
@quota_project_id ||= json["quota_project_id"]
637-
@client = init_client json, options
652+
@client = self.class.init_client json, options
638653
end
639654

640655
def setup_logging logger: :default

lib/googleauth/credentials_loader.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,21 @@ def load_gcloud_project_id
158158
nil
159159
end
160160

161+
# @private
162+
# Loads a JSON key from an IO object, verifies its type, and rewinds the IO.
163+
#
164+
# @param json_key_io [IO] An IO object containing the JSON key.
165+
# @param expected_type [String] The expected credential type name.
166+
# @raise [Google::Auth::InitializationError] If the JSON key type does not match the expected type.
167+
def load_and_verify_json_key_type json_key_io, expected_type
168+
json_key = MultiJson.load json_key_io.read
169+
json_key_io.rewind # Rewind the stream so it can be read again.
170+
return if json_key["type"] == expected_type
171+
raise Google::Auth::InitializationError,
172+
"The provided credentials were not of type '#{expected_type}'. " \
173+
"Instead, the type was '#{json_key['type']}'."
174+
end
175+
161176
private
162177

163178
def interpret_options scope, options

lib/googleauth/default_credentials.rb

Lines changed: 59 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -43,61 +43,81 @@ class DefaultCredentials
4343
# information, refer to [Validate credential configurations from external
4444
# sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
4545
#
46+
# @deprecated This method is deprecated and will be removed in a future version.
47+
# Please use the `make_creds` method on the specific credential class you intend to load,
48+
# e.g., `Google::Auth::ServiceAccountCredentials.make_creds`.
49+
#
50+
# This method does not validate the credential configuration. The security
51+
# risk occurs when a credential configuration is accepted from a source that
52+
# is not under your control and used without validation on your side.
53+
#
54+
# If you know that you will be loading credential configurations of a
55+
# specific type, it is recommended to use a credential-type-specific
56+
# `make_creds` method.
57+
# This will ensure that an unexpected credential type with potential for
58+
# malicious intent is not loaded unintentionally. You might still have to do
59+
# validation for certain credential types. Please follow the recommendation
60+
# for that method. For example, if you want to load only service accounts,
61+
# you can use:
62+
# ```
63+
# creds = Google::Auth::ServiceAccountCredentials.make_creds
64+
# ```
65+
# @see Google::Auth::ServiceAccountCredentials.make_creds
66+
#
67+
# If you are loading your credential configuration from an untrusted source and have
68+
# not mitigated the risks (e.g. by validating the configuration yourself), make
69+
# these changes as soon as possible to prevent security risks to your environment.
70+
#
71+
# Regardless of the method used, it is always your responsibility to validate
72+
# configurations received from external sources.
73+
#
74+
# See https://cloud.google.com/docs/authentication/external/externally-sourced-credentials for more details.
75+
#
4676
# @param options [Hash] Options for creating the credentials
4777
# @return [Google::Auth::Credentials] The credentials instance
4878
# @raise [Google::Auth::InitializationError] If the credentials cannot be determined
4979
def self.make_creds options = {}
5080
json_key_io = options[:json_key_io]
51-
if json_key_io
52-
json_key, clz = determine_creds_class json_key_io
81+
json_key, clz = determine_creds_class json_key_io
82+
if json_key
5383
io = StringIO.new MultiJson.dump(json_key)
5484
clz.make_creds options.merge(json_key_io: io)
5585
else
56-
clz = read_creds
5786
clz.make_creds options
5887
end
5988
end
6089

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
65-
def self.read_creds
66-
env_var = CredentialsLoader::ACCOUNT_TYPE_VAR
67-
type = ENV[env_var]
68-
raise InitializationError, "#{env_var} is undefined in env" unless type
69-
case type
70-
when "service_account"
71-
ServiceAccountCredentials
72-
when "authorized_user"
73-
UserRefreshCredentials
74-
when "external_account"
75-
ExternalAccount::Credentials
76-
else
77-
raise InitializationError, "credentials type '#{type}' is not supported"
78-
end
79-
end
80-
8190
# Reads the input json and determines which creds class to use.
8291
#
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
86-
def self.determine_creds_class json_key_io
87-
json_key = MultiJson.load json_key_io.read
88-
key = "type"
89-
raise InitializationError, "the json is missing the '#{key}' field" unless json_key.key? key
90-
type = json_key[key]
91-
case type
92-
when "service_account"
93-
[json_key, ServiceAccountCredentials]
94-
when "authorized_user"
95-
[json_key, UserRefreshCredentials]
96-
when "external_account"
97-
[json_key, ExternalAccount::Credentials]
92+
# @param json_key_io [IO, nil] An optional IO object containing the JSON key.
93+
# If nil, the credential type is determined from environment variables.
94+
# @return [Array(Hash, Class)] The JSON key (or nil if from environment) and the credential class to use
95+
# @raise [Google::Auth::InitializationError] If the JSON is missing the type field or has an unsupported type,
96+
# or if the environment variable is undefined or unsupported.
97+
def self.determine_creds_class json_key_io = nil
98+
if json_key_io
99+
json_key = MultiJson.load json_key_io.read
100+
key = "type"
101+
raise InitializationError, "the json is missing the '#{key}' field" unless json_key.key? key
102+
type = json_key[key]
98103
else
99-
raise InitializationError, "credentials type '#{type}' is not supported"
104+
env_var = CredentialsLoader::ACCOUNT_TYPE_VAR
105+
type = ENV[env_var]
106+
raise InitializationError, "#{env_var} is undefined in env" unless type
107+
json_key = nil
100108
end
109+
110+
clz = case type
111+
when ServiceAccountCredentials::CREDENTIAL_TYPE_NAME
112+
ServiceAccountCredentials
113+
when UserRefreshCredentials::CREDENTIAL_TYPE_NAME
114+
UserRefreshCredentials
115+
when ExternalAccount::Credentials::CREDENTIAL_TYPE_NAME
116+
ExternalAccount::Credentials
117+
else
118+
raise InitializationError, "credentials type '#{type}' is not supported"
119+
end
120+
[json_key, clz]
101121
end
102122
end
103123
end

lib/googleauth/external_account.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ class Credentials
3434
MISSING_CREDENTIAL_SOURCE = "missing credential source for external account".freeze
3535
INVALID_EXTERNAL_ACCOUNT_TYPE = "credential source is not supported external account type".freeze
3636

37+
# @private
38+
# @type [::String] The type name for this credential.
39+
CREDENTIAL_TYPE_NAME = "external_account".freeze
40+
3741
# Create a ExternalAccount::Credentials
3842
#
3943
# @param options [Hash] Options for creating credentials
@@ -49,6 +53,7 @@ def self.make_creds options = {}
4953
json_key_io, scope = options.values_at :json_key_io, :scope
5054

5155
raise InitializationError, "A json file is required for external account credentials." unless json_key_io
56+
CredentialsLoader.load_and_verify_json_key_type json_key_io, CREDENTIAL_TYPE_NAME
5257
user_creds = read_json_key json_key_io
5358

5459
# AWS credentials is determined by aws subject token type

lib/googleauth/service_account.rb

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ class ServiceAccountCredentials < Signet::OAuth2::Client
4141
attr_reader :project_id
4242
attr_reader :quota_project_id
4343

44+
# @private
45+
# @type [::String] The type name for this credential.
46+
CREDENTIAL_TYPE_NAME = "service_account".freeze
47+
4448
def enable_self_signed_jwt?
4549
# Use a self-singed JWT if there's no information that can be used to
4650
# obtain an OAuth token, OR if there are scopes but also an assertion
@@ -60,15 +64,13 @@ def self.make_creds options = {}
6064
:audience, :token_credential_uri
6165
raise ArgumentError, "Cannot specify both scope and target_audience" if scope && target_audience
6266

63-
if json_key_io
64-
private_key, client_email, project_id, quota_project_id, universe_domain = read_json_key json_key_io
65-
else
66-
private_key = unescape ENV[CredentialsLoader::PRIVATE_KEY_VAR]
67-
client_email = ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
68-
project_id = ENV[CredentialsLoader::PROJECT_ID_VAR]
69-
quota_project_id = nil
70-
universe_domain = nil
71-
end
67+
private_key, client_email, project_id, quota_project_id, universe_domain =
68+
if json_key_io
69+
CredentialsLoader.load_and_verify_json_key_type json_key_io, CREDENTIAL_TYPE_NAME
70+
read_json_key json_key_io
71+
else
72+
creds_from_env
73+
end
7274
project_id ||= CredentialsLoader.load_gcloud_project_id
7375

7476
new(token_credential_uri: token_credential_uri || TOKEN_CRED_URI,
@@ -190,6 +192,20 @@ def apply_self_signed_jwt! a_hash
190192
alt.logger = logger
191193
alt.apply! a_hash
192194
end
195+
196+
# @private
197+
# Loads service account credential details from environment variables.
198+
#
199+
# @return [Array<String, String, String, nil, nil>] An array containing private_key,
200+
# client_email, project_id, quota_project_id, and universe_domain.
201+
def self.creds_from_env
202+
private_key = unescape ENV[CredentialsLoader::PRIVATE_KEY_VAR]
203+
client_email = ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
204+
project_id = ENV[CredentialsLoader::PROJECT_ID_VAR]
205+
[private_key, client_email, project_id, nil, nil]
206+
end
207+
208+
private_class_method :creds_from_env
193209
end
194210
end
195211
end

0 commit comments

Comments
 (0)