Secure Firebase Cloud Function with an API key

Pierre Monier
9 min readApr 9, 2023
Photo by Growtika on Unsplash

Introduction

Firebase Cloud Functions is a serverless compute platform that enables developers to create backend logic in JavaScript or TypeScript. It is built on top of Google Cloud Functions, providing a scalable and flexible backend infrastructure. With Cloud Functions, developers can focus on building features and let Firebase handle the underlying infrastructure, making it an efficient and scalable way to build backend services. Cloud Functions are seamlessly integrated with Firebase, enabling developers to add value to their project directly and quickly deploy code

Firebase Cloud Functions are built on top of Google Cloud Functions, but they provide a simpler and more streamlined interface, with minimal configuration required. This is because Firebase aims to make it easy for developers to deploy backend logic without worrying about infrastructure management.

However, there are times when additional configuration is necessary, such as when restricting access to Cloud Functions. By default, Firebase Cloud Functions are public, but it’s possible to restrict access to specific users or entities. In this article, we’ll explore how to protect Cloud Functions with an API key, so that only authorized users with a valid key can call the function.

Where should we check that a user have correct authorization ?

There are various methods to secure Cloud Functions, and one approach is to check whether the user has the appropriate role within the function. While this can be effective, it may not be the most secure solution. Allowing unauthorized users to enter the function, even if they don’t have permission to call it, could pose a risk. Malicious users could attempt to access sensitive data or execute unauthorized actions.

To mitigate this risk, it’s best to have a reverse proxy in front of the Cloud Function to filter authorized users. This approach helps to ensure that only authorized users can access the function, and provides an additional layer of security against malicious attacks.

Instead of doing that :

in code auth check

We want to do that :

This article primarily focuses on securing calls that are not made from your app’s frontend. If you’re looking to secure Firebase resources in this context, consider using the App Check Firebase service.

Why use API keys to secure Firebase Cloud Functions?

In a reverse proxy, there are multiple techniques available to verify user authorization. One such technique is using an API key, which is relatively easy to implement. However, if we want to enhance security, we can use JWT tokens instead. This approach provides more security because JWT tokens have a limited lifetime, meaning that Cloud Functions can only be called within a specific time range. This approach helps to prevent malicious actors from using a stolen JWT token for an extended period. However, using JWT tokens requires creating and managing them, which can be a bit more complex.

On the other hand, API key authorization requires fewer steps. However, if an API key is stolen, the malicious actor can use it to call Cloud Functions as long as the key is valid. For this reason, it’s important to use API key authorization for non-critical Cloud Functions with a low level of risk. For sensitive Cloud Functions, it’s better to use an authentication method with a limited lifetime to enhance security.

Now that we have all the information we need, let’s move on to the implementation.

Implementation

Disabled public access

I have my Cloud Function, define by the following code :

export const securedHelloWorld =
functions.https.onRequest((request, response) => {
response.send("You are authorized to receive this Hello from Firebase!");
});

Currently, it is not very secure because anyone can call it with a curl command, for example :

curl https://myfirebaseproject.cloudfunctions.net/securedHelloWorld

you will receive :

“You are authorized to receive this Hello from Firebase!”

The first step in securing our Cloud Function is to block public calls. We can do this by going to the GCP interface. When you create a Firebase project, it automatically creates a GCP project. To access your GCP project, the easiest way is to click on “Detailed usage stat.”

This will open your function details in GCP. Now, we need to remove the “allUsers” group from the Cloud Invoker role. This is what allows anyone to call our function. Essentially, it translates to “Ok GCP, let anyone call this function.”

If we try to call our function again with curl, we will receive the following response:

<html><head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<title>403 Forbidden</title>
</head>
<body text=#000000 bgcolor=#ffffff>
<h1>Error: Forbidden</h1>
<h2>Your client does not have permission to get URL <code>/securedHelloWorld</code> from this server.</h2>
<h2></h2>
</body></html>

Now, our function is secured from public calls!

Create a service account

Before creating our API Gateway, we need to give permission to something to call our cloud function. Then we will associate this permission with our API Gateway to let our authorized users call the cloud function. In GCP, we do this with Service Accounts. Service Accounts are non-human accounts on your GCP project used to access specific resources. The best practice is to be as granular as possible and give as little permission as possible to each Service Account. One Service Account should only do one thing.

If you’ve never heard of Service Accounts before, this video is a good resource: What are Service Accounts?

In our case, we will need to have the right to call cloud functions, and we need to let the user who requests our function to act as this Service Account. For this, we will use the “Cloud Function Invoker” role and the “Service Account User” role.

Let’s configure this from the GCP interface:

Create the API config

Now, let’s create a reverse proxy using GCP.

GCP provides a service called “API Gateway” that enables us to create an API gateway, which is a specific type of reverse proxy.

If you’re interested in learning more about the difference between a reverse proxy and an API Gateway, check out this StackOverflow post: https://stackoverflow.com/questions/35756663/api-gateway-vs-reverse-proxy

The GCP API Gateway service work like this:

We have an API with a configuration that determines how the API behaves and a gateway that serves as an endpoint to call it. To secure our Firebase Cloud Function with an API key, we need to create a YAML file that describes our API configuration. This YAML file follows the OpenAPI standard and specifies that we want our API to redirect only authorized users, who have the correct API key, to our cloud function. This configuration can be translated into the following YAML file:

swagger: "2.0"
info:
title: secured-api
description: Secured API to let only authorized users to access the cloud function
version: 1.0.0
schemes:
- https
produces:
- application/json
paths:
/hello:
get:
summary: Receive a hello world message
operationId: hello
x-google-backend:
address: https://myfirebaseproject.cloudfunction.net/securedHelloWorld
protocol: h2
security:
- api_key: []
responses:
"202":
description: Messages received, will be treated
schema:
type: string
"400":
description: Bad request
schema:
type: string
securityDefinitions:
api_key:
type: "apiKey"
name: "key"
in: "query"

That file is quite verbose, let’s summarize what it’s all about.

We can see that this YAML file defines a title and description, which are used to set the name and description of the API in GCP. The paths section specifies the endpoint that we can call. In this case, we have a configuration for the /hello path that allows for an HTTP GET request. The security section specifies that requests require an API key defined by the key api_key. The securityDefinitions section defines the type of security as an apiKey, and specifies that the API key needs to be provided in the query as the value of the key parameter. This YAML file can be used to create a secure API that only allows authorized users to access the Cloud Function.

Now that our API configuration is complete, we can proceed to create it on GCP using the following command:

gcloud api-gateway api-configs create CONFIG_ID \
--api=API_ID --openapi-spec=path-to-yaml-file \
--project=projectid --backend-auth-service-account=service-account-email

This command requires several arguments. The CONFIG_ID and API_ID are unique identifiers for the API and its corresponding configuration. Note that if you haven’t created an API first, it will automatically create one for you using the given ID.

It is important to note that the IDs must follow a specific format, and you should avoid using special characters or keeping them too long.

You also need to provide the path to your configuration file, your project ID, and the identifier of your service account that was created earlier. This identifier corresponds to the email address visible from the GCP console.

Finally, we need to enable the managed service associated with the API by running the following command:

gcloud services enable managed-service

You can find your managed service name at the landing page of your API Gateway’s.

Deploy your API to a Gateway

Now that we have set up everything for our API, the last thing to do is to actually deploy it on a gateway, to let users call it. To do that, we need to run the following command:

 gcloud api-gateway gateways create GATEWAY_ID \
--api=API_ID --api-config=API_CONFIG_ID \
--location=GCP_LOCATION --project=PROJECT_ID

Here, the GATEWAY_ID is how you want to name the Gateway. API_ID and API_CONFIG_ID are IDs that we have defined before. The project ID is still the ID of your GCP project. You also need to define a location. Here is the list of available locations:

* asia-northeast1
* australia-southeast1
* europe-west1
* europe-west2
* us-east1
* us-east4
* us-central1
* us-west2
* us-west3
* us-west4

And now, if you go back to GCP, you will see your API Gateway, and you can copy/paste the URL endpoint to try it!

Create the API Key

Now, if you try to call your endpoint with a curl command without including an API key, you will receive an error response since you need to include the API key to access the resource. Let’s create an API Key with GCP, which can be easily done in the console:

Pretty easy, right? But we should go a step further for security purposes. We need to restrict the scope of this API key to limit its power as much as possible. We must restrict its usage to only the API we defined earlier. This can also be easily done using the GCP console:

Testing and troubleshooting API key security

Everything is done! Good job! Now we have to test that everything works correctly. Here is the checklist that you need to verify:

  • Calling your Firebase Cloud Function directly must fail
  • Calling your API Gateway without an API key must fail
  • Calling your API Gateway with the wrong API key must fail
  • Calling your API Gateway with the correct API key must succeed

If these steps are valid, you have successfully secured your Firebase Cloud Function with an API key! Congrats!

To go a step further, you could monitor strange usage of your API key in order to detect if someone has stolen it. This will not be covered here, but maybe in another article we’ll see

Conclusion

That’s all for this article, I hope you have learned something and had a good time reading it. Don’t forget that an API key might not be the perfect way to secure a Firebase Cloud Function. As always, it depends on the context. If you have any ways to improve this tutorial, feel free to share them in the comments.

👏 Please clap this article if you found it useful 👏

--

--

Pierre Monier

Developer interested in a lot of subjects. Like to create stuff with code and love to learn new things every day.