This is a mirror of Victor's answer, with the exception that it uses IAMCredentialsClient to sign the token instead of the deprecated iam.projects.serviceAccounts.signBlob. This code is a complete TS file so you can drop it in to your project and use it as follows:
const prov = new ApplicationDefaultCredentials(workspaceAdminEmail@mydomain.com)
return prov.getAccessToken(scopes)
Note that the email is only needed if you need to access Workspace APIs via domain-wide-delegation. In other words, this class will work anywhere you need to generate a token to access Google resources, even if you do not provide an email. This class would not needed if you are accessing the resources through Google libraries, but is handy if you are using REST APIs and need to get an access token.
/**
* This class mimics the Application Default Credentials flow and works both in devel and production.
* * It exposes a getAccessToken() method that can be used to get a Bearer token for Authentication and Authorization.
* * The generated token will also work for google workspace APIs calls that are authorized with domain wide delegation.
* When requiring access to Workspace APIs with domain wide delegation, an emailToImpersonate must be provided to the constructor.
* * In Development mode, you will need to get set the GOOGLE_APPLICATION_CREDENTIALS to a key file that
* you created for the service account
* * In production, you do not need to use a service account key at all because this code will generate the key dynamically
* based on the service account specified with the Application Default Credentials. It does this by generating a JWT token,
* using the service account to sign it, and then requesting a token based on that.
* * To use this class, Create an instance via:
* const provider = new ApplicationDefaultCredentials(emailToImpersonate?: string))
* Then use it like this:
* const bearerToken = provider.getAccessToken(scopes)
* */
import { Compute, GoogleAuth, JWT } from 'google-auth-library'
import { IAMCredentialsClient } from '@google-cloud/iam-credentials'
const querystring = require('querystring')
export class ApplicationDefaultCredentials {
scopes: string[] = []
emailToImpersonate?: string
token?: any
tokenExpiry?: Date
constructor(emailToImpersonate?: string) {
this.emailToImpersonate = emailToImpersonate
this.token = undefined
this.tokenExpiry = undefined
}
async getAccessToken(scopes: string[]): Promise<any> {
// Use this hack because "this" in the context of the Promise's anonymous function does not refer to this class.
const self = this
return new Promise(async (resolve, reject) => {
if (self.scopes !== scopes) {
self.token = undefined
self.tokenExpiry = undefined
}
self.scopes = scopes
if (self.token && self.tokenExpiry && self.tokenExpiry > new Date()) {
console.log('Reusing token')
return resolve(self.token)
}
try {
let clientOptions: any = {}
if (self.emailToImpersonate) clientOptions = { subject: self.emailToImpersonate }
const auth = new GoogleAuth({
scopes: scopes,
clientOptions
})
const client = await auth.getClient()
if (client instanceof JWT) {
const json = await client.getAccessToken()
self.token = json.token
// the token expiry does not seem to be returned. If it is undefined, the token will simply not be cached
self.tokenExpiry = json.res?.data.tokenExpiry
resolve(self.token)
return
}
if (!(client instanceof Compute))
throw new Error(`Unexpected authentication type: ${client.constructor!.name}`)
try {
// Create a JWT Token signed with the service account
// Translated this code from https://stackoverflow.com/questions/60435998/domain-wide-delegation-using-default-credentials-in-google-cloud-run?rq=2
// Also took advice from https://developers.google.com/identity/protocols/oauth2/service-account#delegatingauthority
const serviceAccountEmail = (await auth.getCredentials()).client_email
// Create the JWT Payload
const now = Math.floor(new Date().getTime() / 1000)
const expiry = now + 3600
const payload = JSON.stringify({
aud: 'https://oauth2.googleapis.com/token',
exp: expiry,
iat: now,
iss: serviceAccountEmail,
scope: scopes.join(' '),
sub: self.emailToImpersonate
})
const header = JSON.stringify({
alg: 'RS256',
typ: 'JWT'
})
const iamPayload = `${this.unpaddedB64encode(header)}.${this.unpaddedB64encode(payload)}`
// get the JWT Payload signature
const credentialsClient = new IAMCredentialsClient()
const request = {
name: `projects/-/serviceAccounts/${serviceAccountEmail}`,
payload: this.unpaddedB64encode(iamPayload)
}
const [data] = await credentialsClient.signBlob(request)
if (!data.signedBlob) return reject('Could not sign blob')
// send the signed JWT token
const blob64 = this.unpaddedB64encode(data.signedBlob)
const assertion = `${iamPayload}.${blob64}`
const headers = { 'content-type': 'application/x-www-form-urlencoded' }
const body = querystring.encode({
assertion,
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer'
})
const res = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers,
body
})
let json = await res.json()
if (json.error) throw new Error(`${json.error}: ${json.error_description}`)
if (!json.access_token) throw new Error(`No Access token returned: ${JSON.stringify(json)}`)
self.token = json.access_token
let expiresAt = json.expires_at
? json.expires_at * 1000
: json.expires_in
? json.expires_in * 1000 + Date.now()
: undefined
if (expiresAt) self.tokenExpiry = new Date(expiresAt)
resolve(json.access_token)
} catch (error: any) {
console.error('Error generating JWT Token:', error)
reject(error.message ?? error)
}
} catch (err: any) {
console.error('Error in getAccessToken:', err)
reject(err.message ?? err)
}
})
}
unpaddedB64encode(input: string | Uint8Array) {
return Buffer.from(input).toString('base64').replace(/=*$/, '')
}
}