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.
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:
~/.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.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.- 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
There are many ways to create a client based on how the service account details are stored:
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 - Configure the
GOOGLE_APPLICATION_CREDENTIALS
environment variable
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:
The first command logs you in. The second command sets thegcloud auth login gcloud config set project <YOUR_GCP_PROJECT_HERE> gcloud auth application-default login
<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.
- 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.
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']
})
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.
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)
})
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' } })