Cloud functions in GCP (Google Cloud Platform) are a lightweight, stateless, and serverless option for executing code that is triggered by an event. They are equivalent to lambdas in AWS (Amazon Web Services) or Azure Functions in Microsoft Azure. Events that trigger a cloud function include Pub/Sub messages, Cloud Storage events like creating or deleting a storage object, and HTTP requests.
As with any cloud service, you want to make sure that the people and resources interacting with your cloud function are authorized to do so. With Pub/Sub and Cloud Storage triggers the responsibility for restricting who has the ability to invoke your cloud function is deferred to the IAM (Identity and Access Management) associated with those resources, however with HTTP triggers you will have to manage permissions by setting up and assigning one or more Google account and/or service account with the authorization to invoke your cloud function.
By default, a new cloud function created in the GCP Console will require authentication. When deploying a new cloud function from the gcloud
SDK, you will be asked whether or not you would like to allow unauthenticated invocations.
gcloud functions deploy my-new-function `
--entry-point handle_request`
--runtime python38 `
--trigger-http
Allow unauthenticated invocations of new function
[my-new-function]? (y/N)?
By allowing unauthenticated invocations, you are making your cloud function's endpoint available to the open internet. If allowing unauthenticated invocations is the desired behavior, you can bypass this question when deploying via the SDK by using the optional flag --allow-unauthenticated
with the gcloud functions deploy
command. Unauthenticated cloud function invocations should be an exception; for most use cases you will want some form of authentication and authorization.
Depending on who or what is invoking your cloud function the process for setting up authentication will vary, however there are two requirements common to all types of authentication:
- The person or service authorized to invoke the cloud function must be assigned the
cloudfunctions.invoker
role or some other role with thecloudfunctions.invoke
permission. - The person or service authorized to invoke the cloud function must send a token along with the HTTP request to prove that they are authorized to invoke the cloud function.
Describing the process for all use cases is beyond the scope of this article. Instead the focus will be on setting up authentication for one cloud function to invoke another cloud function and on setting up authentication locally so that you can test your secure cloud functions.
Function-to-Function Invocation
Before setting up authentication you will need to have a function written in one of the supported languages with a few lines of code covering the basics of a response to an HTTP request. All of the example code in this post has been written in Python however the principles will be the same for all supported languages.
This first cloud function is the caller function and it is responsible for validating the initial HTTP request, applying some business logic to the contents of that request, and finally invoking the second cloud function with an HTTP request that is authenticated by a token.
import os
import requests
from dotenv import load_dotenv
load_dotenv()
CALLED_CLOUD_FUNCTION_URL = os.getenv('CALLED_CLOUD_FUNCTION_URL')
def send_request_to_called_cloud_function(request):
request_body = request.get_json(silent=True)
if not is_valid_request(request_body):
return 'bad request, missing required field "foo"', 400
called_func_request = prepare_called_func_request(request_body)
token = request_token()
headers = create_request_headers(token)
called_func_response = requests.post(CALLED_CLOUD_FUNCTION_URL, json=called_func_request, headers=headers)
return called_func_response.text, called_func_response.status_code
To create the token, the caller cloud function makes an HTTP request to its metadata server.
def request_token():
metadata_server_url = 'http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience='
token_request_url = metadata_server_url + CALLED_CLOUD_FUNCTION_URL
headers = {'Metadata-Flavor': 'Google'}
token_response = requests.get(token_request_url, headers=headers)
token = token_response.text
return token
def create_request_headers(token):
headers = {
'content-type': 'application/json',
'authorization': f'bearer {token}'
}
return headers
Whether it's a virtual machine, a cloud function, or an app engine app, all of the compute resources in GCP have a server where relevant metadata about the resource is stored. The request to the metadata server url in the request_token
function is checking the caller cloud function's metadata and verifying that the service account attached to it is authorized to invoke the intended audience of the request, the called cloud function. If the service account for the caller was setup correctly, your function will receive a valid token to send to the second cloud function.
How do you create a service account that is authorized to invoke a cloud function? You can either create a new service account using the Console or gcloud
SDK and give it the cloudfunctions.invoker
role, or you can manage your IAM resources using an IAC (Infrastructure as Code) solution like Terraform.
resource "google_service_account" "caller_cloud_function_sa" {
project = "my-cloud-function-project"
account_id = "caller-cloud-function-sa"
display_name = "Caller Cloud Function Service Account"
}
resource "google_cloud_functions_function" "caller_cloud_function" {
project = "my-cloud-function-project"
name = "caller-cloud-function"
entry_point = "send_request_to_called_cloud_function"
runtime = "python38"
service_account_email = google_service_account.caller_cloud_function_sa.email
trigger_http = true
}
resource "google_cloudfunctions_function_iam_member" "cloud_function_invoker" {
project = google_cloud_functions_function.caller_cloud_function.project
region = google_cloud_functions_function.caller_cloud_function.region
cloud_function = google_cloud_functions_function.caller_cloud_function.name
role = "roles/cloudfunctions.invoker"
member = "serviceAccount:${google_service_account.caller_cloud_function_sa.email}"
}
With the service account created and the caller cloud function setup to request a token from its metadata server, the only thing left to do is write the code for the called cloud function
from flask import jsonify
def respond_to_caller_cloud_function_request(request):
request_body = request.get_json(silent=True)
if not is_valid_request(request_body):
return 'bad request, missing required field "bar"', 400
new_entity = save_request_body(request_body)
return jsonify(new_entity), 201
and deploy both cloud functions from the Console, the gcloud
SDK, or through some other means like Terraform.
# using powershell and the gcloud sdk to deploy both cloud functions
gcloud functions deploy caller-cloud-function `
--entry-point send_request_to_called_cloud_function `
--runtime python38 `
--service-account caller-cloud-function-sa@my-cloud-function-project.iam.gserviceaccount.com `
--trigger-http `
--allow-unauthenticated
gcloud functions deploy called-cloud-function `
--entry-point respond_to_caller_cloud_function_request `
--runtime python38 `
--trigger-http
Note that the caller function is currently setup to allow unauthenticated access. To make this example more secure, you will either want to change this function to use one of the other triggers or set up HTTP authentication for it as well.
Testing Your Secured Cloud Function
Now that the called cloud function is secure, testing it from your local machine will require you to send a token along with the request just like the caller cloud function does.
To get a token, first make sure you are logged into your Google account using the gcloud
SDK, and that you have the authorization to invoke a cloud function. This requires the cloudfunctions.invoker
role or any other role that includes the cloudfunctions.invoke
permission.
If you have been assigned one of the basic roles of editor
or owner
you will have the cloudfunctions.invoke
permission already, otherwise you will need to check the list of roles/permissions assigned to your account and possibly request to have a role with the cloudfunctions.invoke
permission added by someone on the project with the ability to grant IAM roles.
To log into a Google account using the gcloud
SDK, use the command
gcloud auth login
This will open up a tab in your browser where you will be prompted to log in using your account's email address, password, and any multi-factor authentication required by your account's security settings.
Once you are logged in, you can request a token with the command
gcloud auth print-identity-token
You can also do this programmatically using Python
def request_identity_token():
stream = os.popen('gcloud auth print-identity-token')
token = stream.read()
return token.strip()
and then send your HTTP request in the same way the caller function does.
import requests
from dotenv import load_dotenv
load_dotenv()
CALLED_CLOUD_FUNCTION_URL = os.getenv('CLOUD_FUNCTION_URL')
def send_test_request():
content = {'foo': 'bar'}
token = request_identity_token()
headers = { 'content-type': 'application/json', 'authorization': f'bearer {token}'
response = requests.post(CALLED_CLOUD_FUNCTION_URL, json=content, headers = headers)
With that you are ready to start deploying HTTP cloud functions that require authentication and to test those functions from your local machine. If your use case requires authorization for end users or some other service or resource the particulars may vary but the general process of attaching the cloudfunctions.invoker
role to a Google account or service account and having the user or resource create a token before calling the function will be the same.
Top comments (1)
One thing i am not understanding, if someone is having access to the caller function, they can get the AUTH token and if they got the AUTH token they can access our secured called function. Then how is that secure as the caller function allows unauthenticated access to get the token. Someone please clarify I am not getting this answer for a while now. Thank you.