In a later article I’ll show how to use the SuperFetch gtb plugin for Gotenberg (converts pretty much anything to PDF files) running on Google Cloud Run
First though, let’s look at in general, how Cloud Run authentication works.
Cloud Run and Cloud Function Authentication with IAM
Both of these need a JWT Id token – not the usual access token that we can easily get from ScriptApp.getOAuthToken() to access most APIs, but the kind of token returned by ScriptApp.getIdentityToken() to identify the current Apps Script user.
This will work for Cloud Functions, but Cloud Run is looking for the identity token of a Service Account. So we need to find a way of generating one of those.
Service Account key files
One way is to use a service account key file (there are plenty of examples of using Service Account with my cGoa library on this site, for example Using a service account) and also creating JWT tokens from Apps Script (for example JWT tokens)
However all of that is a lot of work. Guillaume’s post pointed the way to using Service Accounts without the need for a key file.
Generating an idToken on behald of a Service account with IAM
Create or reuse a cloud project and assign it to your Apps Script project in place of the default one that Apps Script creates (You can’t change the settings on that Apps Script managed cloud project to do what we need)
Give the service account an IAM role that authorizes it to do the work (in this case “Cloud Run Invoker”)
Make the user account (the email address running the Apps Script) a principal for the service account
Give the user account the IAM role (Service Account Token Creator) that authorizes it to generate tokens on behalf of someone else
Enable the IAM API
Give your Apps Script project “https://www.googleapis.com/auth/cloud-platform” and “https://www.googleapis.com/auth/script.external_request” scopes
When that’s all done, the IAM section of the cloud console should have an entry like something like this.
// create a new SuperFetch instance const superFetch = new SuperFetch({ fetcherApp: UrlFetchApp, tokenService: ScriptApp.getOAuthToken })
// create an iam instance const iam = new Plugins.Iam({ superFetch })
// now we can set up how to get an id token const idTokens = iam.tokens({ serviceAccountEmail: gtbServiceAccountEmail }).id({ audience: gtbEndpoint })
// this returns a function that can generate a token console.log(idTokens.service())
setting up idtoken framework
Using the idToken service
Later on in this article, I’ll breifly introduce the gotenberg SuperFetch plugin, but for now you can just access your cloud run instance as normal like this.
As usual I’m using Simple but powerful Apps Script Unit Test library for testing. Details at the end of the article. Here’s a complete set of tests for the Iam.tokens plugin trying out all the methods
const testIam = () => {
// control which tests to skip const skipTest = { tokens: false, caching: false } // get a testing instabce const unit = new bmUnitTester.Unit({ showErrorsOnly: true })
// get an id token for this end point using this service account const gtbEndpoint = SETTINGS.gtb.endPoint const gtbServiceAccountEmail = SETTINGS.gtb.serviceAccountEmail
// test all methods unit.section(() => { // create a new SuperFetch instance - we don't need caching const superFetch = new SuperFetch({ fetcherApp: UrlFetchApp, tokenService: ScriptApp.getOAuthToken }) // create an iam instance const iam = new Plugins.Iam({ superFetch })
// produce id tokens const idTokens = iam.tokens({ serviceAccountEmail: gtbServiceAccountEmail }).id({ audience: gtbEndpoint })
unit.is('object', typeof idTokens, { description: 'idTokens namespace is an object' }) unit.is('function', typeof idTokens.service, { description: 'idTokens.service is a function' }) unit.is('string', typeof idTokens.token, { description: 'idTokens.token is a string' }) unit.is(null, idTokens.fullService().throw().error, { description: 'full service returns a packResponse' })
// caching should never happen no matter the cache settings unit.section(() => {
// create a new SuperFetch instance const superFetch = new SuperFetch({ fetcherApp: UrlFetchApp, tokenService: ScriptApp.getOAuthToken, cacheService: CacheService.getUserCache() }) // create an iam instance const iam = new Plugins.Iam({ superFetch, noCache: false })
// now we can set up how to get an id token const idTokens = iam.tokens({ serviceAccountEmail: gtbServiceAccountEmail }).id({ audience: gtbEndpoint })
unit.is('object', typeof idTokens.fullService().throw().data, { description: 'full service returns an object' }) unit.is('object', typeof idTokens, { description: 'idTokens namespace is an object' }) unit.is('function', typeof idTokens.service, { description: 'idTokens.service is a function' }) unit.is('string', typeof idTokens.token, { description: 'idTokens.token is a string' }) unit.not(true, idTokens.fullService().throw().cached, { description: 'token still didnt come from cache as its a POST' })
}, { description: 'iam credentials with caching is not possible', skip: skipTest.caching }) }
iam.tokens test
SuperFetch idTokenService plugin
There’s a handy plugin in the SuperFetch library to simplify all that. Here’s how to use it. Just supply your serviceAccount and cloud run endpoint and you’ll get back a function that can generate an id token.
// use this basic setup so we can get an idTokenservice const superFetch = new SuperFetch({ fetcherApp: UrlFetchApp, tokenService: ScriptApp.getOAuthToken })
// use this for a special token service const gtbTokenService = idTokenService({ superFetch, serviceAccountEmail: gtbServiceAccountEmail, audience: gtbEndpoint })
SuperFetch.Plugins.idTokenService
You can use this token to authenticate to any cloud run service that requires authentication. My service looks like this.
Gtb, Cnv and Drv SuperFetch plugins
I’ll cover these plugins in detail in separate articles, but SuperFetch also has plugins for the Drive REST Api, a conversion Api for PDF’s and the Gotenberg API running on cloud run. Here’s how to apply everything I’ve covered in this article to getting pretty much any kind of Office/image type file from drive, converting it to a PDF and writing the converted file back to Drive
// get an id token for this end point using this service account const gtbEndpoint = SETTINGS.gtb.endPoint const gtbServiceAccountEmail = SETTINGS.gtb.serviceAccountEmail
// use this basic setup so we can get an idTokenservice const superFetch = new SuperFetch({ fetcherApp: UrlFetchApp, tokenService: ScriptApp.getOAuthToken })
// use this for a special token service const gtbTokenService = idTokenService({ superFetch, serviceAccountEmail: gtbServiceAccountEmail, audience: gtbEndpoint })
// we'll need a different superFetch to authenticate to cloud run const gtbSuperFetch = new SuperFetch({ fetcherApp: UrlFetchApp, tokenService: gtbTokenService, })
// get instances of the plugins we'll need const drv = new Drv({ superFetch }) const gtb = new Gtb({ superFetch: gtbSuperFetch, endPoint: gtbEndpoint }) const cnv = new Cnv({ gtb })
// get an image to convert const blob = drv.files.path({ path: 'filmid/mixedartwork/Chrysler.jpeg' }).download().throw().blob
// convert it to a pdf const pdf = cnv.exec({ blob, convertTo: 'pdf' }).throw().blob
// write it back to drive drv.files.path({path: 'converted/Chrysler.pdf', blob: pdf}).upload().throw()
full example
Testing Cnv and Gtb plugins
And here’s the tests for the Cnv and gtb plugins
const testCnv = () => {
const skipTest = { gtb: false } // get a testing instabce const unit = new bmUnitTester.Unit({ showErrorsOnly: true })
// get an id token for this end point using this service account const gtbEndpoint = SETTINGS.gtb.endPoint const gtbServiceAccountEmail = SETTINGS.gtb.serviceAccountEmail
// use this basic setup so we can get an idTokenservice const superFetch = new SuperFetch({ fetcherApp: UrlFetchApp, tokenService: ScriptApp.getOAuthToken })
// use this for a special token service const gtbTokenService = idTokenService({ superFetch, serviceAccountEmail: gtbServiceAccountEmail, audience: gtbEndpoint })
// we'll need a different superFetch to authenticate to cloud run const gtbSuperFetch = new SuperFetch({ fetcherApp: UrlFetchApp, tokenService: gtbTokenService, })
unit.section(() => { const drv = new Drv({ superFetch }) const gtb = new Gtb({ superFetch: gtbSuperFetch, endPoint: gtbEndpoint }) const cnv = new Cnv({ gtb }) unit.is(`up`, gtb.health().throw().data.status, { description: 'gtb is running' }) // get an image to convert const img = unit.not( null, drv.files.path({ path: 'filmid/mixedartwork/Chrysler.jpeg' }).download().throw().blob, { description: 'get an image to convert' }) // convert it to a pdf const pdf = unit.not( null, cnv.exec({ blob: img.actual, convertTo: 'pdf' }).throw().blob, { description: 'convert using gtb' }) unit.is('application/pdf', drv.files.path({path: 'converted/Chrysler.pdf'}).upload({blob: pdf.actual}).throw().data.mimeType, { description: 'uploaded the converted file and mimeType is good' }) }, { skip: skipTest.gtb, description: 'Check gtb is all good' })
bruce mcpherson is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License. Based on a work at http://www.mcpher.com. Permissions beyond the scope of this license may be available at code use guidelines