I've been trying to set up a google cloud function (2nd gen with http trigger) and can't seem to get past a 401 unauthorized. I got it working using an identity token I was generating via the gcloud cli but I need to use a service account to avoid the identity token's 1 hr expiration time.
I have set cloud function invoker and cloud run invoker permissions on the service account as well as connected the service account to the cloud function directly. Despite what I do, I cannot get it to authorize using the service account.
I'm trying to authenticate from a Rails app using the googleauth gem. Below is my service.
# app/services/cloud_function_service.rb
class CloudFunctionService
# Use the credentials and URL from Rails credentials or environment
def self.api_request(function_type, body_data)
new.api_request(function_type, body_data)
end
def api_request(function_type, body_data)
access_token = fetch_access_token
uri = Rails.application.credentials.CLOUD_FUNCTION_URL
body = {
'function_type': function_type,
'body': body_data
}.to_json
headers = {
'Authorization' => "Bearer #{access_token}",
'Content-Type' => 'application/json'
}
# Make the POST request with headers and body
result = HTTParty.post(uri, body: body, headers: headers)
JSON.parse(result.body)
rescue => e
Rails.logger.error "Failed to call cloud function: #{e}"
nil
end
private
def fetch_access_token
authorizer = Google::Auth::ServiceAccountCredentials.make_creds(
json_key_io: File.open('./google-service-account-keys.json'),
scope: 'https://www.googleapis.com/auth/cloud-platform'
)
response = authorizer.fetch_access_token!
response['access_token']
end
end
Answering my own question here. There were actually two issues that may be helpful for anyone invoking a cloud function from a Ruby on Rails app.
First, I misunderstood the usage of access token vs. identity token but you do actually need both. The access token is required to hit google API endpoints (which cloud function endpoints are not considered a part of). The identity token is needed to hit cloud function endpoints. But... you use the access token to get the identity token.
Second, unlike other language libraries for Google Auth the Ruby library doesn't provide a method to get the identity token. It only provides a method to get the access token. You need to build your own api handler for the identity token endpoint.
Basically, the flow looks like this:
Here is my final code that works!
class CloudFunctionService
def self.api_request(function_type, body_data)
new.api_request(function_type, body_data)
end
def api_request(function_type, body_data)
access_token = fetch_access_token
service_account_email = _your_gcp_service_account_email_
target_audience = _your_cloud_function_uri_
identity_token = fetch_identity_token(service_account_email, target_audience, access_token)
uri = _your_cloud_function_uri_
body = {
# Your body data
}.to_json
headers = {
'Authorization' => "Bearer #{identity_token}",
'Content-Type' => 'application/json'
}
result = HTTParty.post(uri, body: body, headers: headers)
JSON.parse(result.body)
rescue => e
Rails.logger.error "Failed to call cloud function: #{e}"
nil
end
private
def fetch_access_token
decoded_json_key = Base64.decode64(_your_base64_encoded_json_key_file)
json_key_io = StringIO.new(decoded_json_key)
authorizer = Google::Auth::ServiceAccountCredentials.make_creds(
json_key_io: json_key_io,
scope: 'https://www.googleapis.com/auth/cloud-platform'
)
authorizer.fetch_access_token!['access_token']
end
def fetch_identity_token(service_account_email, target_audience, access_token)
url = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/#{service_account_email}:generateIdToken"
body = {
audience: target_audience,
includeEmail: true
}.to_json
response = HTTParty.post(
url,
body: body,
headers: {
'Content-Type' => 'application/json',
'Authorization' => "Bearer #{access_token}"
}
)
if response.code == 200
JSON.parse(response.body)['token']
else
raise "Failed to fetch identity token: #{response.body}"
end
end
end