2020-04-03
GCP Credentials & Next.js

Let's begin with the problem. You're running a Next.js app on ZEIT Now, you've created some API functions and now you want to make an authenticated call to some GCP service using their client libraries (Firebase, GCS, Big Query, etc). For apps not running on GCP, you must provide your own set of credentials to make authenticated requests to your GCP services, however, we don't want to just go and store those credentials in our repo as plaintext (🚨 danger️️ous 🚨)! We should use some secure data-store for this. Luckily, if you're deploying on Now, it has support for secrets, but they only allow the secret value to be a string.

$ now secrets add <secret-name> <secret-value>

Wouldn't it be nice if we could store our service account JSON  in that secret? Turns out we can with the power of base64 encoding. With a few commands we can turn our JSON key into a secret that can be consumed in our API functions.

Here are the steps we'll need to do

  1. Create a GCP service account with the appropriate permissions.
  2. Download a JSON credential for that service account.
  3. Convert that service account into a base64 encoded string and save it as a Now Secret.
  4. Configure the build processes (remote and local) of Now to access this secret and store it as an environment variable.
  5. Configure Next.js to expose this environment variable to the application.
  6. Read the environment variable in our API function, base64 decode it and create a Google Credential from that key.
  7. Make authenticated requests to your GCP services like Firebase, GCS, BigQuery, etc.

Creating and managing the Service account

First you'll need to create a service account in GCP with the appropriate permissions granted to it, Google has a simple guide explaining how to do this. Next, download a JSON key of this account. Once you have your JSON key, you can rename it if you'd like to match the following shell command. Now that we have our JSON key, we can create a Now secret using the data in it.

$ now secret add <secret-name> $(cat service-account.json | base64)

ZEIT Now configuration

Now that your service account is encoded via base64 and stored in Now. We need to set up a few more things in the build process for that secret to be accessible by your API function. We have two cases we need to cover, one, your local development build will need to read that secret and two, the remote deployment of Next.js on Now. Using Now build configuration we will tell the Now deployment to mount that secret into our Next app configuration so we can access the secret as an environment variable.

For the local case, we will create a new file called .env.build at your project root. You will need to copy the base64 encoded secret into this file. Be sure to add this file to your .gitignore or else your secret may become public!

$ echo GOOGLE_APPLICATION_CREDENTIALS=$(cat service-account.json | base64) >> .env.build
$ echo .env.build >> .gitignore

Now instead of starting up your service with npm run dev or yarn dev you will need to start using now dev check this blog post for more info.

For the remote case, you will need to create a file in the root called now.json and populate it as follows.

{
  "build": {
    "env": {
      "GOOGLE_APPLICATION_CREDENTIALS": "@secret-name"
    }
  }
}

Be sure to note the "@" symbol, this tells Now to use a secret of this name instead of the raw string.

Next.js Configuration

Next up we want to configure Next to expose this environment variable to the application. To do so, modify your next.config.js. If you don't already have one, create an empty file at the root again and name it next.config.js. Add the following to that file. Check out the Next docs for more info on using a custom next.config.js.

module.exports = {
  env: {
    GOOGLE_APPLICATION_CREDENTIALS: process.env.GOOGLE_APPLICATION_CREDENTIALS,
  },
};

Accessing the Service Account in the API Function

We have one final step before we can make authenticated calls to GCP. That is reading the environment variable where our secret is stored, and turning it back (base64 decode) into a credential that can be consumed by the various GCP SDKs. Once we do this, we can make all the authenticated requests we like!

const credential = JSON.parse(
    Buffer.from(process.env.GOOGLE_APPLICATION_CREDENTIALS, 'base64').toString()
);

// Authenticate with the GCS SDK
import { Storage } from '@google-cloud/storage';

const storage = new Storage({
    projectId: '<gcp-project-id>',
    credentials: credential,
});

// Authenticate with the Firebase Admin SDK
import * as admin from 'firebase-admin';

admin.initializeApp({
    ...appOptions,
    credential: admin.credential.cert(credential),
});

That just about sums it up. Hope this helps you on your Next project!

If you have any questions, hit me up on Twitter.