DEV Community

Cover image for Setting Up Authorization for HTTP Cloud Functions in GCP
Jake Witcher
Jake Witcher

Posted on

Setting Up Authorization for HTTP Cloud Functions in GCP

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)?
Enter fullscreen mode Exit fullscreen mode

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:

  1. The person or service authorized to invoke the cloud function must be assigned the cloudfunctions.invoker role or some other role with the cloudfunctions.invoke permission.
  2. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
farmanabbasi profile image
Mohammad Farman Abbasi

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.