Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add AWS Workload Identity Federation Support #386

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
78 changes: 78 additions & 0 deletions lib/googleauth/base_client.rb
@@ -0,0 +1,78 @@
# Copyright 2022 Google, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require "os"

module Google
# Module Auth provides classes that provide Google-specific authorization
# used to access Google APIs.
module Auth
# BaseClient is a class used to contain common methods that are required by any
# Credentials Client, including AwsCredentials, ServiceAccountCredentials,
# and UserRefreshCredentials. This is a superclass of Signet::OAuth2::Client
# and has been created to create a generic interface for all credentials clients
# to use, including ones which do not inherit from Signet::OAuth2::Client.
module BaseClient

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this module? Is there a reason that ExternalAccountCredentials can't extend from Signet::Oauth2::Client like the other credential classes in this library, instead of selecting a few of the methods from this module, and creating a superclass for it? It seems like it would just be easier to overwrite the methods from the client that don't work

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thinking here was that the ExternalAccountCredentials uses a completely different OAuth2 flow that is not supported by Signet. Since Signet doesn't support token exchange I figured it would just add confusion for other developers if I pulled in Signet and didn't use any of it's features. The current implementation is the most clear way i could think of to avoid confusion around what Signet does and doesn't do while still DRYing everything up. If you would prefer I just paste everything into the Signet file and include it everywhere I can go ahead and make that change and try it out, just figured I would explain my thinking and get your feedback before making the change.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this makes sense. Let's mention Signet::Oauth2::Client in the comments, and mention that this has been created as a superclass for Signet for Credentials that don't need all of Signet's features

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this more reasonable? a89e880

AUTH_METADATA_KEY = :authorization

# Updates a_hash updated with the authentication token
def apply! a_hash, opts = {}
# fetch the access token there is currently not one, or if the client
# has expired
fetch_access_token! opts if needs_access_token?
a_hash[AUTH_METADATA_KEY] = "Bearer #{send token_type}"
end

# Returns a clone of a_hash updated with the authentication token
def apply a_hash, opts = {}
a_copy = a_hash.clone
apply! a_copy, opts
a_copy
end

# Whether the id_token or access_token is missing or about to expire.
def needs_access_token?
send(token_type).nil? || expires_within?(60)
end

# Returns a reference to the #apply method, suitable for passing as
# a closure
def updater_proc
proc { |a_hash, opts = {}| apply a_hash, opts }
end

def on_refresh &block
@refresh_listeners = [] unless defined? @refresh_listeners
@refresh_listeners << block
end

def notify_refresh_listeners
listeners = defined?(@refresh_listeners) ? @refresh_listeners : []
listeners.each do |block|
block.call self
end
end

def expires_within?
raise "This method must be implemented by a subclass"
end

private

def token_type
raise "This method must be implemented by a subclass"
end
end
end
end
5 changes: 5 additions & 0 deletions lib/googleauth/credentials_loader.rb
Expand Up @@ -30,6 +30,11 @@ module CredentialsLoader
REFRESH_TOKEN_VAR = "GOOGLE_REFRESH_TOKEN".freeze
ACCOUNT_TYPE_VAR = "GOOGLE_ACCOUNT_TYPE".freeze
PROJECT_ID_VAR = "GOOGLE_PROJECT_ID".freeze
AWS_REGION_VAR = "AWS_REGION".freeze
AWS_DEFAULT_REGION_VAR = "AWS_DEFAULT_REGION".freeze
AWS_ACCESS_KEY_ID_VAR = "AWS_ACCESS_KEY_ID".freeze
AWS_SECRET_ACCESS_KEY_VAR = "AWS_SECRET_ACCESS_KEY".freeze
AWS_SESSION_TOKEN_VAR = "AWS_SESSION_TOKEN".freeze
GCLOUD_POSIX_COMMAND = "gcloud".freeze
GCLOUD_WINDOWS_COMMAND = "gcloud.cmd".freeze
GCLOUD_CONFIG_COMMAND = "config config-helper --format json --verbosity none".freeze
Expand Down
5 changes: 5 additions & 0 deletions lib/googleauth/default_credentials.rb
Expand Up @@ -18,6 +18,7 @@
require "googleauth/credentials_loader"
require "googleauth/service_account"
require "googleauth/user_refresh"
require "googleauth/external_account"

module Google
# Module Auth provides classes that provide Google-specific authorization
Expand Down Expand Up @@ -53,6 +54,8 @@ def self.read_creds
ServiceAccountCredentials
when "authorized_user"
UserRefreshCredentials
when "external_account"
ExternalAccountCredentials
else
raise "credentials type '#{type}' is not supported"
end
Expand All @@ -69,6 +72,8 @@ def self.determine_creds_class json_key_io
[json_key, ServiceAccountCredentials]
when "authorized_user"
[json_key, UserRefreshCredentials]
when "external_account"
[json_key, ExternalAccountCredentials]
else
raise "credentials type '#{type}' is not supported"
end
Expand Down
67 changes: 67 additions & 0 deletions lib/googleauth/external_account.rb
@@ -0,0 +1,67 @@
# Copyright 2015 Google, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require "time"
require "googleauth/credentials_loader"
require "googleauth/external_account/aws_credentials"

module Google
# Module Auth provides classes that provide Google-specific authorization
# used to access Google APIs.
module Auth
# Authenticates requests using External Account credentials, such
# as those provided by the AWS provider.
class ExternalAccountCredentials
# The subject token type used for AWS external_account credentials.
AWS_SUBJECT_TOKEN_TYPE = "urn:ietf:params:aws:token-type:aws4_request".freeze
AWS_SUBJECT_TOKEN_INVALID = "aws is the only currently supported external account type".freeze

attr_reader :project_id
attr_reader :quota_project_id

# Create a ExternalAccountCredentials
#
# @param json_key_io [IO] an IO from which the JSON key can be read
# @param scope [string|array|nil] the scope(s) to access
def self.make_creds options = {}
json_key_io, scope = options.values_at :json_key_io, :scope

raise "a json file is required for external account credentials" unless json_key_io
user_creds = read_json_key json_key_io

raise AWS_SUBJECT_TOKEN_INVALID unless user_creds["subject_token_type"] == AWS_SUBJECT_TOKEN_TYPE
Google::Auth::ExternalAccount::AwsCredentials.new(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a few different types of external account credential files that exist right now. Let's make sure the file is intended to be AWS credentials before we use that class to create them, and raise an "unknown credential" error until we've implemented them. Use either of these methods:

In GoLang, we make sure that the environment ID of the credential source is "AWS1" before we create AWS credentials (and could be AWS2, AWS3, etc for future versions of these credentials)

In Python, we make sure that the subject token type of the credential source is "urn:ietf:params:aws:token-type:aws4_request"

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 994456a

audience: user_creds["audience"],
scope: scope,
subject_token_type: user_creds["subject_token_type"],
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The token_url and service_account_impersonation_url need to be validated.

See the java implementation: https://github.com/googleapis/google-auth-library-java/blob/main/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java#L219

token_url: user_creds["token_url"],
credential_source: user_creds["credential_source"],
service_account_impersonation_url: user_creds["service_account_impersonation_url"]
)
end

# Reads the required fields from the JSON.
def self.read_json_key json_key_io
json_key = MultiJson.load json_key_io.read
wanted = [
"audience", "subject_token_type", "token_url", "credential_source"
]
wanted.each do |key|
raise "the json is missing the #{key} field" unless json_key.key? key
end
json_key
end
end
end
end