Skip to content

Instantly share code, notes, and snippets.

@flamelier
Forked from nicolasdao/google-auth-library.md
Created November 2, 2022 13:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save flamelier/794f83a778bef1566a79b203c25450e3 to your computer and use it in GitHub Desktop.
Save flamelier/794f83a778bef1566a79b203c25450e3 to your computer and use it in GitHub Desktop.
google-auth-library doc because I did not get the info I wanted out of the official doc. Keywords: google-auth-library gcp token security auth

google-auth-library GUIDE

The google-auth-library NPM package is used to manage accesses to most of the GCP APIs and resources. For example, a typical scenario is to acquire an OAuth2 access token or an ID token.

This document is an attempt to provide a different spin to the official Google documentation and explain how this library works and how to use it properly.

Table of contents

What you need to know about identities before you start

In the many examples you'll see on the web (incl. Google's own doc), you'll see the following kind of code snippet to create a new auth client:

const {GoogleAuth} = require('google-auth-library')

/**
 * Instead of specifying the type of client you'd like to use (JWT, OAuth2, etc)
 * this library will automatically choose the right client based on the environment.
 */
async function main() {
  const auth = new GoogleAuth({
    scopes: 'https://www.googleapis.com/auth/cloud-platform'
  })
  const client = await auth.getClient()
  const projectId = await auth.getProjectId()
  const url = `https://dns.googleapis.com/dns/v1/projects/${projectId}`
  const res = await client.request({ url })
  console.log(res.data)
}

main().catch(console.error)

The line of interest for this section is:

const auth = new GoogleAuth({
  scopes: 'https://www.googleapis.com/auth/cloud-platform'
})

This is the recommended approach to create an auth client (it is also possible to explicitly configure the credentials as explained later in this document). However, this snippet could make you wonder how the code above gets its secrets to make authorized requests. The answer is not clearly explained in the official documentation. It gets the credentials by automatically looking in various default places and throws an exception of they are are not found. The default places I'm aware of are:

  1. ~/.config/gcloud/application_default_credentials.json(1), which is equivalent to being invited by the SysAdmin to the project and granted specific privileges. The steps to set that file up are details in the Configure a ~/.config/gcloud/application_default_credentials.json file section.
  2. GOOGLE_APPLICATION_CREDENTIALS environment variable. If this variable exists, it must be a path to a service account JSON key file on the hosting machine.
  3. Explicit HTTP request to the Metadata server (only used when hosted on GCP). The credentials are cached after that request.

The first two options above are usefull when you are hosting your app locally or in a non-GCP environment. The third one is the approach used when your app is hosted on GCP (i.e., Cloud Compute, App Engine, Cloud Function or Cloud Run). The Metadata server stores, amongst other things, the service account associated with the instance/container. To learn more about this topic, please refer to the official doc at https://cloud.google.com/compute/docs/storing-retrieving-metadata.

(1) On Windows the path is [APPDATA_FOLDER]/gcloud/application_default_credentials.json

How to create a client?

There are many ways to create a client based on how the service account details are stored:

Using the implicit hosting environment identity

The next snippet is the recommended way to create an auth client if your app is hosted on GCP (i.e., Cloud Compute, App Engine, Cloud Function or Cloud Run).

const { GoogleAuth } = require('google-auth-library')

const auth = new GoogleAuth()

The secret credentials used behind new GoogleAuth() are automatically set up when your app is hosted on GCP (more details about this in the What you need to know about identities before you start section). Those credentials belong to the service identity associated with your GCP hosting environment (i.e., a service account). If your app fails with 403 or 401 errors, this is most likely due to a misconfiguration of the roles associated with that service account.

To use this snippet when developing on your local machine, or hosting in a non-GCP environment, choose one of those two methods:

Configure a ~/.config/gcloud/application_default_credentials.json file

Google refers this method as Application Default Credential (ADC). It is equivalent to being invited by the SysAdmin to the project and granted specific privileges.

  • Make sure you have a Google account that can access both the GCP project and the resources you need on GCP.
  • Install the GCloud CLI on your environment.
  • Execute the following commands:
     gcloud auth login
     gcloud config set project <YOUR_GCP_PROJECT_HERE>
     gcloud auth application-default login
    
    The first command logs you in. The second command sets the <YOUR_GCP_PROJECT_HERE> as your default project. Finally, the third command creates a new ~/.config/gcloud/application_default_credentials.json file with the credentials you need for the <YOUR_GCP_PROJECT_HERE> project.

Configure the GOOGLE_APPLICATION_CREDENTIALS environment variable

  • Request from your SysAdmin a service account JSON key file that has access to the project's resources you need.
  • Save that file on your hosting environment, and copy the its path.
  • Set and enviroment variable called GOOGLE_APPLICATION_CREDENTIALS and set its value to the path copied previously.

Using the service account details explicitly

const { GoogleAuth } = require('google-auth-library')

const auth = new GoogleAuth({
	credentials: {
		client_email: serviceAccount.client_email,
		private_key: serviceAccount.private_key
	}
})

If the client is used to acquire an access token, scopes are also required:

const auth = new GoogleAuth({
	credentials: {
		client_email: serviceAccount.client_email,
		private_key: serviceAccount.private_key
	},
	scopes:['https://www.googleapis.com/auth/cloud-platform']
})

How to get an ID token?

OAuth2 ID tokens are required to access protected web API hosted on GCP (e.g., Cloud Function, Cloud Run).

const { GoogleAuth } = require('google-auth-library')

// If this code runs inside GCP, you can simply use: const auth = new GoogleAuth()
// The credentials will be the ones of the service account associated with the hosting environment (e.g., Cloud Run, Cloud Function)
const auth = new GoogleAuth({
	credentials: {
		client_email: serviceAccount.client_email,
		private_key: serviceAccount.private_key
	}
})

const audience = 'https://your-app-ts.a.run.app' // Example of a protected Cloud Run web endpoint
const client = yield auth.getIdTokenClient(audience)
client.getRequestMetadataAsync().then(({ headers }) => {
	const idToken = headers.Authorization.replace('Bearer ', '')
	console.log(idToken)
})

Trick: If you're interested in learning how to hack an id_token to pass custom claims in it and disguise it as an access_token to access protected Cloud Function or Cloud Run, please refer to this article.

How to get an access token?

OAuth2 access tokens are required to access the Google APIs.

const { GoogleAuth } = require('google-auth-library')

// If this code runs inside GCP, you can simply use: const auth = new GoogleAuth()
// The credentials will be the ones of the service account associated with the hosting environment (e.g., Cloud Run, Cloud Function)
const auth = new GoogleAuth({
	credentials: {
		client_email: serviceAccount.client_email,
		private_key: serviceAccount.private_key
	},
	scopes: ['https://www.googleapis.com/auth/cloud-platform']
})

auth.getAccessToken().then(accessToken => {
	console.log(accessToken)
})

How to create a self-signed JWT token?

Unfortunately, google-auth-library does not support this scenario. This section shows how to build this feature yourself. The use case for this approach is to generate ID tokens with custom claims.

WARNING: To be able to run the next piece of code, you must first:

  • Enable the 'iamcredentials.googleapis.com' service in your project.
  • Have a service account and extract a new JSON key.
  • Add the role roles/iam.serviceAccountTokenCreator on that service account so it is able to request self-signed JWT.
const { co } = require('core-async')
const { GoogleAuth } = require('google-auth-library')
const jwt = require('jsonwebtoken')
const { fetch } = require('./src/utils')

const serviceAccount = {
	client_email: '****',
	private_key: '****'
}

co(function *(){
	const auth = new GoogleAuth({
		credentials: serviceAccount,
		scopes: ['https://www.googleapis.com/auth/cloud-platform']
	})
	
	const accessToken = yield auth.getAccessToken()
	
	const nowEpocSeconds = Math.floor(Date.now()/1000)

	const resp = yield fetch.post({
		uri: `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${serviceAccount.client_email}:signJwt`,
		headers: {
			'Content-Type': 'application/json',
			Authorization: `Bearer ${accessToken}`
		},
		body: {
			delegates: [],
			payload: JSON.stringify({ 
				iss: serviceAccount.client_email, 
				sub: serviceAccount.client_email, 
				aud: 'https://oauth2.googleapis.com/token', 
				iat: nowEpocSeconds, 
				exp: nowEpocSeconds + 3600,
				others: {
					hello: 'world'
				}
			})
		}
	})
	
	console.log('SELF SIGNED JWT')
	console.log(jwt.decode(resp.data.signedJwt))
})

If you need to exchange that ID token agains another ID token for access to other systems:

const otherResp = yield fetch.post({
	uri: 'https://oauth2.googleapis.com/token',
	headers: {
		'Content-Type': 'application/json'
	},
	body: {
		grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
		assertion: resp.data.signedJwt
	}
})

console.log(jwt.decode(otherResp.data.id_token, { complete:true }))

INFO: If the last piece of code is intended to acquire an ID token for access to a protected Cloud Run or a protected Cloud Function, you need to add the target_audience claim in your self-signed JWT:

payload: JSON.stringify({ 
	iss: serviceAccount.client_email, 
	sub: serviceAccount.client_email, 
	aud: 'https://oauth2.googleapis.com/token', 
	iat: nowEpocSeconds, 
	exp: nowEpocSeconds + 3600,
	target_audience: 'https://your-cloud-run-service.run.app/',
	others: {
		hello: 'world'
	}
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment