How to implement domain-wide delegation in GSuite using Node.js

In this post I'll show you how you can use domain-wide delegation to have a Node.js based web service act on behalf of users within a Google Suite organization.

Google can easily drown you in documentation but let me just show you how it's done.

First, you need to setup a service account within G Suite. The guide for that is quite adequate.

After jumping through the hoops above you should be able to download a .json based service account key file that looks like this:

{
  "type": "service_account",
  "project_id": "acme-project",
  "private_key_id": "some-id",
  "private_key": "long-crazy-key",
  "client_email": "descriptive-name@some-service.iam.gserviceaccount.com",
  "client_id": "some-id",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/descriptive-name%40some-service.iam.gserviceaccount.com"
}

For this Node.js based project I recommend storing client_email and the private_key within a .env file living in your code project. This .env file is then never committed to version control. Using the dotenv package you can then load those secret variables into your running program's environment.

Read on, through the forest of code and I'll explain on the other side.

// loads credentials from .env file
require("dotenv").config();
import { google } from "googleapis";

function initializeDrive(version = "v3") {
  const client_email = process.env.GOOGLE_CLIENT_EMAIL;
  // add some necessary escaping so to avoid errors when parsing the private key.
  const private_key = process.env.GOOGLE_PRIVATE_KEY.replace(/\\n/g, "\n");
  // impersonate an account with rights to create team drives
  const emailToImpersonate = "some-user@acme-industries.com";
  const jwtClient = new google.auth.JWT(
    client_email,
    null,
    private_key,
    ["https://www.googleapis.com/auth/drive"],
    emailToImpersonate
  );
  return google.drive({
    version: version,
    auth: jwtClient
  });
}

The above code uses the Google APIs library for Node.js. It takes the key file credentials, creates a jwtClient instance and use it to call google.drive(). When called the initializeDrive() returns a object with which you can access the Google Drive API.

Also note, that the service accounts is set to impersonate a google user. Normally Google users would authenticate against a service to give that service access to their data. But in other cases you want to allow a service to act without asking for efficiency.

In a project I was building we needed a super user to automatically administrate a set of team drives. So, then we first manually created that user in GSuite before configuring our web service to act on behalf of that user.

Let's consider an example where we list out team drives:

export const listTeamDrives = async ({ pageToken = "" } = {}) => {
  const drive = initializeDrive("v3");
  return new Promise((resolve, reject) => {
    drive.teamdrives.list(
      {
        pageSize: 100,
        ...(pageToken ? { pageToken } : {})
      },
      function(err, { data: { nextPageToken = "", teamDrives = [] } = {} }) {
        if (err) {
          return reject(err);
        }
        if (!nextPageToken) {
          return resolve(teamDrives);
        }
        // if page token is present we'll recursively call ourselves until
        // we have a complete team drive list.
        return listTeamDrives({ pageToken: nextPageToken }).then(
          otherTeamDrives => {
            resolve(teamDrives.concat(otherTeamDrives));
          }
        );
      }
    );
  });
};

// an example of how the listTeamDrives() function would be called
const main = async () => {
  try {
    teamDriveFolders = await listTeamDrives();
    console.log({ teamDriveFolders });
  } catch (e) {
    console.error("Failed to list folders", e);
  }
};

main();

If this code looks bewildering I can recommend reading up on: Destructuring assignment, spread syntax, default parameters, promises and async/await.

The code above instantiates an instance of the drive api and then calls to list all team drives the user has access to (see API docs). The upper limit for drives is 100 but we had to allow for more so I made the function call recursively call itself if the response would include a nextPageToken. Also note that the function drive.teamdrives.list() is a callback but we wrap it in a promise for easier use elsewhere (the docs said it supports promises but this call didn't when I tried).

This post was motivated by some hefty integration work at my workplace, Netlife Design. This web service I've mentioned now integrates Sanity CMS, GSuite and Auth0. And I want to give a shoutout to my colleague Haakon Borch who fought valiantly alongside me when we were diving into the Google API documention.

Hope this helps you save time. Please give me a shoutout on Twitter if it does. :)