SuperFetch is a proxy for UrlFetchApp with additional features – see SuperFetch – a proxy enhancement to Apps Script UrlFetch for how it works and what it does. In this article I’ll cover how to use the SuperFetch api plugin to access IAM Service Account Credentials to authenticate with Cloud Run.

Motivation

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.

Background

This was quite tricky to figure out, but thanks to fellow GDEs Spencer Easton and Ivan Kutil and this post by guillaume blaquiere I was able to add the technique to my SuperFetch library

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

It turns out that we can use the IAM service account credential API to do exactly what’s needed.

Preparation

In preparation we need to

  • 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.
service account
The email address of my service account is
cloud-run-invoker@MYPROJECT.iam.gserviceaccount.com

Add the user as a principal

Edit the service account and assign the Service Account creator role
service account creator roe

Now you should have this on the permissions screen for your cloud-run-invoker service account

assigned

SuperFetch  and iam plugin

You’ll need the SuperFetch library – details at the end of the article.

The preparation was the hard part. Now we can use the SuperFetch iam plugin to get a drop in replacement for the regular tokenService.

    // get an id token for this end point using this service account
    const gtbEndpoint = 'https://xxxxxx.run.app'
  
    // your service account
    const gtbServiceAccountEmail = 'cloud-run-invoker@xxxxxxxx.iam.gserviceaccount.com'    

	// imports
    const { Plugins, SuperFetch } = bmSuperFetch

    // 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.


headers.authorization = 'Bearer ' + idTokens.service()
using idToken

Unit testing for Iam plugin

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

  // import required modules
  const { Plugins, SuperFetch } = bmSuperFetch

  // 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'
    })

  }, {
    description: 'iam tokens',
    skip: skipTest.tokens
  })

  // 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.

  const { Plugins, SuperFetch } = bmSuperFetch
  const { idTokenService } = Plugins


  // 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.

cloud run service

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

  // import required modules
  const { Plugins, SuperFetch } = bmSuperFetch
  const { idTokenService, Drv, Gtb, Cnv } = Plugins


  // 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

  // import required modules
  const { Plugins, SuperFetch } = bmSuperFetch
  const { idTokenService, Drv, Gtb, Cnv } = Plugins


  // 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'
  })

}
testing cnv and gtb

Links

bmSuperFetch: 1B2scq2fYEcfoGyt9aXxUdoUPuLy-qbUC2_8lboUEdnNlzpGGWldoVYg2

IDE

GitHub

bmUnitTester: 1zOlHMOpO89vqLPe5XpC-wzA9r5yaBkWt_qFjKqFNsIZtNJ-iUjBYDt-x

IDE

GitHub

goa twitter oauth2 apps script

Apps Script Oauth2 – a Goa Library refresher

It's been a few years since I first created the Goa library. Initially it was mainly to provide OAuth2 authorization ...
Read More
SuperFetch

SuperFetch plugin – Firebase client for Apps Script

Frb is a SuperFetch plugin to easily access a Firebase Real time database. SuperFetch is a proxy for UrlFetchApp with ...
Read More
SuperFetch

SuperFetch plugin – iam – how to authenticate to Cloud Run from Apps Script

SuperFetch is a proxy for UrlFetchApp with additional features - see SuperFetch - a proxy enhancement to Apps Script UrlFetch for ...
Read More
SuperFetch

SuperFetch – a proxy enhancement to Apps Script UrlFetch

I've written a few articles about JavaScript proxying on here, and I'm a big fan. I also use a lot ...
Read More
SuperFetch

Apps script caching with compression and enhanced size limitations

Motivation Caching is a great way to improve performance, avoid rate limit problems and even save money if you are ...
Read More

Simple but powerful Apps Script Unit Test library

Why unit testing? There are many test packages for Node (my favorite is ava) and there are also a few ...
Read More