How to Secure API Keys with 2nd-Gen Cloud Functions and Firebase

Firebase Cloud Functions allow you to write backend code that can be triggered in response to events, and they are very useful if you want to add some server-side logic to your Flutter apps.

But 1st-gen Cloud Functions have some limitations, and the introduction of the 2nd Generation of Cloud Functions for Firebase brings some important benefits:

  • Fewer cold starts, which has been a big pain point for a long time.
  • Improved concurrency, since each instance of a function can now handle multiple requests.
  • Secure and type-safe API keys with parameterized configuration, which are better than the environment variables that were used by default on 1st-gen functions.

This official announcement on the Firebase blog offers a good overview of the new features.

But after attempting to migrate one of my projects from 1st-gen to 2nd-gen Cloud Functions, I got stuck on the API keys migration, and I could not easily find all the information I needed:

  • How to set API keys with the Firebase CLI from the command line
  • How to access them inside 2nd-gen Cloud Functions (both https functions and Firestore triggers)
  • How to access the API keys when testing functions with the Firebase Local Emulator

So if you also want to use 2nd-gen Cloud Functions and store your API keys securely, this step-by-step guide is for you.

To use a real-world scenario as an example, I’ll show how to configure and read some Stripe API keys inside a Cloud Function, but the same considerations apply to any other API keys that need to be stored on your Firebase backend. 👍

This guide is about how to store API keys securely on the server using Firebase. To follow it, you’ll need to already have a Firebase project on the Blaze plan, and have the Firebase CLI installed. If you’re looking for a guide about API-keys on the client, read: How to Store API Keys in Flutter: --dart-define vs .env files.

Ready? Let’s go!

The Old Way, a.k.a. using Environment Variables (and what’s wrong with it)

If you’ve been using 1st-gen functions in the past, chances are you’ve been using firebase functions:config:set to define some environment variables:

# run this to set environment variable from the command line firebase functions:config:set stripe.secret_key=whsec_YOUR_STRIPE_SECRET_KEY firebase functions:config:set stripe.webhook_secret_key=whsec_YOUR_STRIPE_WEBHOOK_SECRET_KEY

Reading these variables inside your Cloud Functions is just a case of using the functions.config() class:

import * as functions from "firebase-functions" import Stripe from "stripe" // 1. read the keys const stripeSecretKey = functions.config().stripe?.secret_key const stripeWebhookSecretKey = functions.config().stripe?.webhook_secret_key // 2. declare a function exports.createOrderPaymentIntent = functions.https.onCall((data, context) { // 3. use the key(s) as needed if (stripeSecretKey === undefined) { throw new functions.https.HttpsError("aborted", "Stripe Secret Key is not set") } // create the Stripe object, passing the key as an argument const stripe = new Stripe(stripeSecretKey as string, { apiVersion: "2023-08-16", typescript: true, }) // use it as needed ... })

However, this approach is not secure for secret values (like API keys). And it doesn’t make any guarantees about whether a value is set or what its type is.

So let’s try to migrate the code above to use 2nd gen functions. 👇

The New Way: 2nd Gen Functions with the Secrets API

For a start, defining secrets is now done using firebase functions:secrets:set:

# note: use UPPER_CASE when defining the key name firebase functions:secrets:set STRIPE_SECRET_KEY ? Enter a value for STRIPE_SECRET_KEY [input is hidden]

This will prompt you to enter the value of the secret key (if you’re using Stripe, this can be found here).

But if it’s the first time you do this in your Firebase project, you’ll get an error:

Error: HTTP Error: 403, Secret Manager API has not been used in project <your-firebase-project-id> before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/secretmanager.googleapis.com/overview?project=<your-firebase-project-id> then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.

Step 1: Enable the Secret Manager API in Google Cloud

The error above contains a link that will take you to the Secret Manager API page in the Google Cloud Console.

Alternatively, you can go to https://console.cloud.google.com and follow these steps. 👇

Step 1a: Select the correct project, then navigate to “Enabled APIs & services”

Step 1a: Navigate to “Enabled APIs & services”
Step 1a: Navigate to “Enabled APIs & services”

Step 1b: Click on “Enable APIs and Services”

Step 1b: Click on “Enable APIs and Services”
Step 1b: Click on “Enable APIs and Services”

Step 1c: Search for “Secret Manager API”

Step 1c: Search for “Secret Manager API”
Step 1c: Search for “Secret Manager API”

Step 1d: Click on the Secret Manager API result

Step 1d: Click on the Secret Manager API result
Step 1d: Click on the Secret Manager API result

Step 1e: Enable the Secret Manager API

Step 1e: Enable the Secret Manager API
Step 1e: Enable the Secret Manager API

Step 2: Set your secrets with the Firebase CLI

Once you’ve enabled the Secret Manager API, you should be able to successfully set your secret (which can be found on the Stripe API keys page):

# note: use UPPER_CASE when defining the key name firebase functions:secrets:set STRIPE_SECRET_KEY ? Enter a value for STRIPE_SECRET_KEY [input is hidden]

Then, you can do the same for the webhook secret (which is generated every time you register a new Stripe webhook)

# note: use UPPER_CASE when defining the key name firebase functions:secrets:set STRIPE_WEBHOOK_SECRET_KEY ? Enter a value for STRIPE_WEBHOOK_SECRET_KEY [input is hidden]

The prompt above shows how to set the Stripe API keys, but this is just an example. The main point is that you should call firebase functions:secrets:set for each API key you have in your project.

Next up, it’s time to update the Cloud Functions. 👇

Step 3: Use the new Secrets API with 2nd-Gen Cloud Functions

The new secrets API works both with 1st-gen and 2nd-gen functions (with some minor differences in syntax).

When using 2nd-gen functions, they can be defined like this:

// 1. import functions V2 import * as functions from "firebase-functions/v2" // 2. import the defineSecret function import {defineSecret} from "firebase-functions/params" import Stripe from "stripe" // 3. define the secrets, using the keys that we previously stored with the Firebase CLI // https://firebase.google.com/docs/functions/2nd-gen-upgrade#special_case_api_keys const stripeSecretKey = defineSecret("STRIPE_SECRET_KEY") const stripeWebhookSecretKey = defineSecret("STRIPE_WEBHOOK_SECRET_KEY")

Then, they should be passed as arguments when declaring onCall and onRequest functions:

// this function needs to access the stripeSecretKey only exports.createOrderPaymentIntent = functions.https.onCall({ secrets: [stripeSecretKey] }, (context) => { // defined elsewhere return createOrderPaymentIntent(context) }) // this function needs to access both keys exports.stripeWebhook = functions.https.onRequest({ secrets: [stripeSecretKey, stripeWebhookSecretKey] }, (request, response) => { // defined elsewhere return stripeWebhook(request, response) })

Finally, the value of each secret key can be obtained with the .value() method:

// Expose a endpoint as a webhook handler for asynchronous events. // Configure your webhook in the Stripe developer dashboard: // https://dashboard.stripe.com/test/webhooks async function stripeWebhook(req: functions.https.Request, res: functions.Response<any>) { // get the first secret key value const secretKey = stripeSecretKey.value() // if it's empty, throw an error if (secretKey.length === 0) { console.error("⚠️ Stripe Secret Key is not set") res.sendStatus(400) } // get the second secret key value const webhookSecretKey = stripeWebhookSecretKey.value() // if it's empty, throw an error if (webhookSecretKey.length === 0) { console.error("⚠️ Stripe webhook secret is not set") res.sendStatus(400) } const stripe = new Stripe(secretKey, { apiVersion: "2023-08-16", typescript: true, }) // do webhook signature verification using the webhookSecretKey }

Note how calling secretKey.value() is guaranteed to return a string. If the key is not set correctly, its length will be 0, and we can return an error.

Step 4: Testing our Cloud Functions with the Firebase Local Emulator

Before deploying the Cloud Functions, it’s always a good idea to test them locally, and this can be done with the Firebase Local Emulator.

If you’re not familiar with the Firebase Local Emulator, read: Flutter Tutorial: How to use the Firebase Local Emulator with Cloud Functions

But as explained in Secrets and credentials in the Cloud Functions emulator, we need to perform an extra step.

And that is to add the keys inside a .secret.local file in the functions folder:

# functions/.secret.local STRIPE_SECRET_KEY=sk_test_YOUR_STRIPE_SECRET_KEY STRIPE_WEBHOOK_SECRET_KEY=whsec_YOUR_STRIPE_WEBHOOK_SECRET_KEY

And since these are very sensitive keys, we’ll also want to add this file to the local .gitignore:

# Secrets .secret.local

That's it! We should now be able to access our secret keys inside the Cloud Functions. And if everything works well, we can deploy them. 🚀

What About Cloud Firestore Triggers?

As we have seen, we can pass secrets to https calls using this syntax:

import * as functionsV2 from "firebase-functions/v2" import {defineSecret} from "firebase-functions/params" const stripeSecretKey = defineSecret("STRIPE_SECRET_KEY") exports.createOrderPaymentIntent = functionsV2.https.onCall( { secrets: [stripeSecretKey] }, (context) => { const secretKey = stripeSecretKey.value() if (secretKey.length === 0) { throw new https.HttpsError("aborted", "Stripe Secret Key is not set") } // all good }, )

But what about Cloud Firestore triggers?

As it turns out, this can be done by passing the document and secrets arguments as a map of key-value pairs:

import * as functionsV2 from "firebase-functions/v2" import {defineSecret} from "firebase-functions/params" const stripeSecretKey = defineSecret("STRIPE_SECRET_KEY") exports.onPaymentWritten = functionsV2.firestore.onDocumentWritten( { document: "/stripe_customers/{stripeId}/payments/{paymentId}", secrets: [stripeSecretKey], }, (event) => { const secretKey = stripeSecretKey.value() if (secretKey.length === 0) { throw new https.HttpsError("aborted", "Stripe Secret Key is not set") } // all good }, )

This was not clear at all since the Firestore triggers have a scary-looking API in TypeScript:

export declare function onDocumentWritten<Document extends string>( opts: DocumentOptions<Document>, handler: (event: FirestoreEvent<Change<DocumentSnapshot> | undefined, ParamsOf<Document>>) => any | Promise<any> ): CloudFunction<FirestoreEvent<Change<DocumentSnapshot> | undefined, ParamsOf<Document>>>;

So if you don't want to get lost in a big rabbit hole, just use the example above when you implement your own triggers. 👍

Wrap Up

We’ve now seen how to set and access secrets securely when using 2nd-gen Cloud Functions for Firebase.

Here’s a summary of the steps:

  1. Enable the Secret Manager API inside Google Cloud for your Firebase project
  2. Store your keys by running firebase functions:secrets:set
  3. Use the new Secrets API with 2nd-Gen Cloud Functions:
    • Define the secrets with the defineSecret API
    • Tell our onRequest and onCall functions which secrets they have access to
    • Access them with .value() inside the functions body
  1. Add a .secret.local file inside the functions folder, store the keys inside it, and add it to .gitignore

While there’s a bit of work involved, this approach is much more secure than the old functions.config() API and works with both 1st-gen and 2nd-gen Cloud Functions.

It took me a while to figure out all these details and I hope this guide will make your life easier. 🙂

Additional resources

Here are some additional resources I found useful while researching this topic:

Happy coding!

New Flutter & Firebase Course

If you want to ship your Flutter apps faster, Firebase is a great choice. And in this new course, I cover all the most important features, including Firebase Auth, Cloud Firestore, Firebase Storage, Cloud Functions, and Firebase Extensions. 👇

Want More?

Invest in yourself with my high-quality Flutter courses.

Flutter In Production

Flutter In Production

Learn about flavors, environments, error monitoring, analytics, release management, CI/CD, and finally ship your Flutter apps to the stores. 🚀

Flutter Foundations Course

Flutter Foundations Course

Learn about State Management, App Architecture, Navigation, Testing, and much more by building a Flutter eCommerce app on iOS, Android, and web.

The Complete Dart Developer Guide

The Complete Dart Developer Guide

Learn Dart Programming in depth. Includes: basic to advanced topics, exercises, and projects. Last updated to Dart 2.15.

Flutter Animations Masterclass

Flutter Animations Masterclass

Master Flutter animations and build a completely custom habit tracking application.