googleapis/google-auth-library-ruby

Authenticate using service account to Cloud Function via HTTP?

denishaskin opened this issue · 1 comments

Environment details

  • OS: macOS 11.2.3
  • Ruby version: ruby 2.7.2p137
  • Gem name and version: googleauth (0.16.0)

Steps to reproduce

  1. Create the file below
  2. set GOOGLE_APPLICATION_CREDENTIALS env var to be a path to a valid GCP service account key file
  3. Ensure the service account in question is listed in the permissions for this Cloud Function with the Cloud Functions Invoker role.
  4. 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.

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