rubygoogle-cloud-functionsgoogle-authenticationgoogle-auth-librarygoogle-auth-library-ruby

Access Cloud Function via HTTP from ruby client, using google auth gem


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

Solution

  • 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.