Authenticate using service account to Cloud Function via HTTP?
denishaskin opened this issue · 1 comments
denishaskin commented
Environment details
- OS: macOS 11.2.3
- Ruby version: ruby 2.7.2p137
- Gem name and version: googleauth (0.16.0)
Steps to reproduce
- Create the file below
- set GOOGLE_APPLICATION_CREDENTIALS env var to be a path to a valid GCP service account key file
- Ensure the service account in question is listed in the permissions for this Cloud Function with the Cloud Functions Invoker role.
- Execute the file
Access fails with the following output.
opening connection to us-east1-xx-beta.cloudfunctions.net:443...
opened
starting SSL for us-east1-xx-beta.cloudfunctions.net:443...
SSL established, protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384
<- "GET /my-cloud-function?foo=bar HTTP/1.1\r\nAccept: application/json\r\nAuthorization: Bearer ya29.c.Kp8B-QfKOgrA5jwGxc3tK8MkX2RJqe0LcQqKtjo1hMDJawXJZWTOq3nZyOHkfHZMJpm5hbFr11T7LFTzbP4bnxX1SIUXJt-papBSKYU0lOKXXiqdwa9s5yN-oHD45qSp6bSOIMwzhG9GJC8ZqeOrbyJAhd5oqmZUgqDg_2cgGusA5wVViaJZtiBFC_TaMmBK06MUmGT2a1q4UGLIyH4nyug1\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nUser-Agent: Ruby\r\nConnection: close\r\nHost: us-east1-xx-beta.cloudfunctions.net\r\n\r\n"
-> "HTTP/1.1 401 Unauthorized\r\n"
-> "WWW-Authenticate: Bearer error=\"invalid_token\" error_description=\"The access token could not be verified\"\r\n"
-> "Date: Mon, 29 Mar 2021 19:42:21 GMT\r\n"
-> "Content-Type: text/html; charset=UTF-8\r\n"
-> "Server: Google Frontend\r\n"
-> "Content-Length: 315\r\n"
-> "Alt-Svc: h3-29=\":443\"; ma=2592000,h3-T051=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"\r\n"
-> "Connection: close\r\n"
-> "\r\n"
reading 315 bytes...
-> "\n<html><head>\n<meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\">\n<title>401 Unauthorized</title>\n</head>\n<body text=#000000 bgcolor=#ffffff>\n<h1>Error: Unauthorized</h1>\n<h2>Your client does not have permission to the requested URL <code>/my-cloud-function?foo=bar</code>.</h2>\n<h2></h2>\n</body></html>\n"
read 315 bytes
Conn close
Code example
require 'googleauth'
class TestService
include HTTParty
format :json
debug_output $stdout
def test
authorizer = Google::Auth::ServiceAccountCredentials.make_creds(
json_key_io: File.open(ENV['GOOGLE_APPLICATION_CREDENTIALS']),
scope: 'https://www.googleapis.com/auth/cloud-platform')
options =
{
headers:
{
accept: 'application/json',
authorization: "Bearer #{authorizer.fetch_access_token!['access_token']}"
},
query:
{
foo: 'bar'
}
}
self.class.get('https://us-east1-xx-beta.cloudfunctions.net/my-cloud-function', options)
end
end
TestService.new.test
I am wondering if I should be specifying audience
instead of scope
, but I've been unable to make that work.
denishaskin commented
I resolved this, it was lack of understanding on my part that an id token needed to be used, not an access token (and the gem is a little implicit about how to do one versus the other). For what it's worth, here's a little authorization helper we're now using:
require 'googleauth'
module Google
# Wrapper for Google's auth gem, handles getting the credentials from file,
# does the right thing for the service we're calling.
class Authorizer
CREDENTIALS_JSON_STRING = File.read(ENV['GOOGLE_APPLICATION_CREDENTIALS']).freeze
def initialize(cloud_function_url: nil)
options = { json_key_io: StringIO.new(CREDENTIALS_JSON_STRING) }
if cloud_function_url.present?
options[:target_audience] = cloud_function_url
end
@_authorizer ||= Google::Auth::ServiceAccountCredentials.make_creds(options)
end
def http_authorization
@_http_authorization ||= { authorization: "Bearer #{id_token}" }
end
def id_token
@_id_token ||= @_authorizer.fetch_access_token!['id_token']
end
end
end
which can be used something like this (using HTTParty):
class MyService
include HTTParty
format :json
def initialize
@my_cloud_function_url = ENV['MY_CLOUD_FUNCTION_URL']
@headers = {
accept: 'application/json',
}.merge(http_authorization)
end
def get(options)
options[:headers] = (options[:headers] || {}).merge(@headers)
self.class.get(@my_cloud_function_url, options)
end
private
def http_authorization
Google::Authorizer.new(cloud_function_url: @my_cloud_function_url).http_authorization
end