Programmatic Authentication with Google Identity-Aware Proxy

One-click deployment for Philips Hue firmware: in this blogpost I summarize the benefits of Google IAP and programmatic authentication.

Programmatic Authentication with Google Identity-Aware Proxy

One-click deployment for Philips Hue firmware

Philips needed some new infrastructure to deploy firmware to Hue devices, so I built a Node.js API for uploading firmware. It has a snazzy Swagger api-docs page for documentation, testing and uploading via the browser. Last, but certainly not least, I needed a way to limit access to only a few people, so I used Google Identity-Aware Proxy (IAP). Which is really easy to set up.

But wait, doing firmware deployment by hand is boring and leaves room for human error! Oh no! How do I get past IAP without humans or a browser? Looks like we’ve got to implement some form of API authentication, but building that is also pretty boring. So sad.

Enter Google’s documentation on programmatic authentication with a service account. It has examples for four languages, but unfortunately not for JavaScript/Node.js. Too bad I was totally unfamiliar with pretty much everything done in the example code. On top of that, the libraries in the examples abstracted away some of the details I needed. So I took a few hours to piece together the missing information from Stack Overflow answers and blog posts. I summarized the whole thing below to save you some time!

Roll up your sleeves, let’s get to work

It all boils down to four steps:

  1. Set up a service account.
  2. Create a JWT token.
  3. Get a bearer token from Google’s OpenID.
  4. Do requests to the IAP secured resource.

The JWT token is used to prove to Google OpenID that you are supposed to have access to the resources you want the bearer token for. When you include this OpenID bearer token in the request header send to the IAP secured resource (a Node.js API endpoint in my case) IAP lets you through.

You can also check out the code on the Q42 GitHub. As this is example code, don’t forget that it’ll only work if you fill in the variables specific to your project.

1. Set up a Service Account (for your client script to use)

  1. Use the Cloud console to create a service account in the project in which you’re using Google IAP.
  2. Enter a sensible name and click OK.
  3. Give the user the “IAP-secured Web App User” role and click OK.
  4. In the final step, click “Create Key” and save the JSON file as serviceAccount.json . Securely delete this after you reach the “2. Generate and sign a JWT” step.

All done there. Just one more thing: get the client ID of the IAP secured resource:

  1. Go here.
  2. Click the three vertical dots next to the IAP secured resource, go to “Edit OAuth Client”.
  3. Copy the “Client ID” at the top of the page.
  4. Make a new secrets.json file and paste your client ID in there, like this:
{
  "clientID": "<the Client ID you copied>",
  "private_key": "<the private key from your serviceAccount.json>",
  "client_email": "<the client email from your serviceAccount.json>",
}

Be careful with this file! Do not ever commit or (publicly) upload these secrets.

2. Generate and sign a JWT

Create a JWT with the following properties:

const myJWTProperties = {
  iss: <client_email property from the `secrets.json`, i.e.: dev-test@firmware-dev.iam.gserviceaccount.com>,
  aud: "https://www.googleapis.com/oauth2/v4/token",
  iat: <current date/time in seconds since epoch, i.e. `Math.floor(new Date().getTime() / 1000)`>,
  exp: <iat + time in seconds the JWT should be valid for>,
  target_audience: <clientID property from the `secrets.json`>,
}

Sign the JWT with the private_key property from the secrets.json file. Use RS256 as algorithm. I’m using the jsonwebtoken package from NPM here, which abstracts away all the JWT creation and signing into this simple one-liner solution:

const myJWT = jsonwebtoken.sign({…myJWTProperties}, privateKey, { algorithm: "RS256" });

3. Get an OpenID token

Send a POST request to the OpenID token URL https://www.googleapis.com/oauth2/v4/token, with the JWT and grant type in the form data.

request.post(openIDTokenURL, {
  grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
  assertion: myJWT,
}, (error, response) => {
  const data = JSON.parse(response.body);
  openIDToken = data.id_token;
  // Do step 3
});

Parse the response body as JSON, it should have a property id_token which contains the id token.

4. Use the OpenID token to perform requests to the secured endpoints

Create an authorization header, use this every time you do a request to the IAP secured endpoints/resource.

const requestHeader = {
  "User-Agent": "c2fIAP",
  "Authorization": `Bearer ${openIDToken}`
};

The authorization header will look something like this: "Authorization": "Bearer c3NTk4IiwiZW1haWwiOiJkZ..."

Now you can start sending requests!

request.get(
  "https://firmware-dev.appspot.com/admin/banana",
  { requestHeader },
  (err, res, body) => console.info("Got a healthy snack:", body)
);

Benefits of Google IAP and programmatic authentication

For me there were three main benefits

  1. I can administer access on a per-service basis, all out of the box!
  2. Our testers can now write tests, instead of performing them manually
  3. Philips can automate the last step of firmware deployment, making them more agile

When you only need API authentication, Google IAP might not always seem like the obvious choice. But the ease of not having to build server side authentication and token management makes it totally worth the token exchange you need to understand, even for Node.js users. In the end, the implementation costs way less effort than I expected, and I hope this blogpost will have made it even easier for you!


Check out our job vacancies (in Dutch) at werkenbij.q42.nl!