Skip to content

Commit

Permalink
Add tests for ExternalAccount, add methods required to make test comp…
Browse files Browse the repository at this point in the history
…atible with apply_auth_examples

ExternalAccount was not fully compatible with the other providers, this brings it in line and moves some common methods between the other provides and ExternalAccount into a module which is now included by the AWSClient provider and Signet::Auth::Client
  • Loading branch information
rbclark committed Jun 27, 2022
1 parent a010945 commit 9e712c5
Show file tree
Hide file tree
Showing 6 changed files with 288 additions and 66 deletions.
76 changes: 76 additions & 0 deletions lib/googleauth/base_client.rb
@@ -0,0 +1,76 @@
# 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 "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.
module BaseClient
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
73 changes: 48 additions & 25 deletions lib/googleauth/external_account.rb
Expand Up @@ -13,6 +13,8 @@
# limitations under the License.

require "time"
require "googleauth/credentials_loader"
require "googleauth/base_client"
require "googleauth/oauth2/sts_client"

module Google
Expand Down Expand Up @@ -51,7 +53,7 @@ def self.make_creds options = {}
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", "service_account_impersonation_url"
"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
Expand All @@ -64,7 +66,8 @@ def self.read_json_key json_key_io
# by utilizing the AWS EC2 metadata service and then exchanging the
# credentials for a short-lived Google Cloud access token.
class AwsCredentials
AUTH_METADATA_KEY = :authorization
include BaseClient

STS_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange".freeze
STS_REQUESTED_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token".freeze
IAM_SCOPE = ["https://www.googleapis.com/auth/iam"].freeze
Expand All @@ -85,7 +88,7 @@ def initialize options = {}
@region = options[:region] || region(options)
@request_signer = AwsRequestSigner.new @region

@expiry = nil
@expires_at = nil
@access_token = nil

@sts_client = Google::Auth::OAuth2::STSClient.new token_exchange_endpoint: @token_url
Expand All @@ -98,42 +101,64 @@ def fetch_access_token! options = {}

if @service_account_impersonation_url
impersonated_response = get_impersonated_access_token response["access_token"]
@expiry = Time.parse impersonated_response["expireTime"]
@access_token = impersonated_response["accessToken"]
self.expires_at = impersonated_response["expireTime"]
self.access_token = impersonated_response["accessToken"]
else
# Extract the expiration time in seconds from the response and calculate the actual expiration time
# and then save that to the expiry variable.
@expiry = Time.now.utc + response["expires_in"].to_i
@access_token = response["access_token"]
self.expires_at = Time.now.utc + response["expires_in"].to_i
self.access_token = response["access_token"]
end

notify_refresh_listeners
end

# Whether the id_token or access_token is missing or about to expire.
def needs_access_token?
@access_token.nil? || expires_within?(60)
def notify_refresh_listeners
listeners = defined?(@refresh_listeners) ? @refresh_listeners : []
listeners.each do |block|
block.call self
end
end

def expires_within? seconds
@expiry && @expiry - Time.now.utc < seconds
@expires_at && @expires_at - Time.now.utc < seconds
end

def expires_at
@expires_at
end

def expires_at= new_expires_at
@expires_at = normalize_timestamp new_expires_at
end

# 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 #{@access_token}"
def access_token
@access_token
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
def access_token= new_access_token
@access_token = new_access_token
end

private

def token_type
:access_token
end

def normalize_timestamp time
case time
when NilClass
nil
when Time
time
when String
Time.parse time
else
raise "Invalid time value #{time}"
end
end

def exchange_token credentials
request_options = @request_signer.generate_signed_request(
credentials,
Expand Down Expand Up @@ -170,9 +195,7 @@ def get_impersonated_access_token token, options = {}
response = c.post @service_account_impersonation_url do |req|
req.headers["Authorization"] = "Bearer #{token}"
req.headers["Content-Type"] = "application/json"
req.body = MultiJson.dump({
scope: @scope
})
req.body = MultiJson.dump({ scope: @scope })
end

if response.status != 200
Expand Down
2 changes: 1 addition & 1 deletion lib/googleauth/oauth2/sts_client.rb
Expand Up @@ -74,7 +74,7 @@ def exchange_token options = {}
request_body = {
grant_type: options[:grant_type],
audience: options[:audience],
scope: options[:scopes]&.join(" ") || [],
scope: Array(options[:scopes])&.join(" ") || [],
requested_token_type: options[:requested_token_type],
subject_token: options[:subject_token],
subject_token_type: options[:subject_token_type]
Expand Down
2 changes: 1 addition & 1 deletion lib/googleauth/service_account.rb
Expand Up @@ -130,7 +130,7 @@ def apply_self_signed_jwt! a_hash
# cf [Application Default Credentials](https://cloud.google.com/docs/authentication/production)
class ServiceAccountJwtHeaderCredentials
JWT_AUD_URI_KEY = :jwt_aud_uri
AUTH_METADATA_KEY = Signet::OAuth2::AUTH_METADATA_KEY
AUTH_METADATA_KEY = Signet::OAuth2::Client::AUTH_METADATA_KEY
TOKEN_CRED_URI = "https://www.googleapis.com/oauth2/v4/token".freeze
SIGNING_ALGORITHM = "RS256".freeze
EXPIRY = 60
Expand Down
42 changes: 3 additions & 39 deletions lib/googleauth/signet.rb
Expand Up @@ -13,16 +13,18 @@
# limitations under the License.

require "signet/oauth_2/client"
require "googleauth/base_client"

module Signet
# OAuth2 supports OAuth2 authentication.
module OAuth2
AUTH_METADATA_KEY = :authorization
# Signet::OAuth2::Client creates an OAuth2 client
#
# This reopens Client to add #apply and #apply! methods which update a
# hash with the fetched authentication token.
class Client
include Google::Auth::BaseClient

def configure_connection options
@connection_info =
options[:connection_builder] || options[:default_connection]
Expand All @@ -34,37 +36,6 @@ def token_type
target_audience ? :id_token : :access_token
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

# 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

# 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

alias orig_fetch_access_token! fetch_access_token!
def fetch_access_token! options = {}
unless options[:connection]
Expand All @@ -78,13 +49,6 @@ def fetch_access_token! options = {}
info
end

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

def build_default_connection
if !defined?(@connection_info)
nil
Expand Down

0 comments on commit 9e712c5

Please sign in to comment.