I'm trying to use the ruby googleauth gem to authenticate a service account to make an HTTP call to a Cloud Function, without success.
Test code:
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.)
I set GOOGLE_APPLICATION_CREDENTIALS env var to be a path to a valid GCP service account key file, and have ensured that the service account in question is listed in the permissions for this Cloud Function with the Cloud Functions Invoker role.
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
As John Hanley mentioned, you are using access token as authorization on your function but you need an Identity Token to successfully call the function. You can do that in your program by changing the scope
to target_audience
:
authorizer = Google::Auth::ServiceAccountCredentials.make_creds(
json_key_io: File.open(ENV['GOOGLE_APPLICATION_CREDENTIALS']),
target_audience: "GCF-TRIGGER-URL")
Then fetch the ID token using:
authorizer.fetch_access_token!["id_token"]
There's a clue in make_creds()
(view source) that specifying both scope and target_audience would raise an ArgumentError. Testing the library would show that specifying scope
returns an access token while specifying target_audience
would return an ID Token.