Page Content hide
1 Motivation

Motivation

Goa is a library to support OAuth2 for Apps Script connecting to a variety of services, using a variety of Authentication flows and processes. There are plenty of other articles on Goa on this site, see Apps Script Oauth2 – a Goa Library refresher to remind you how Goa works. If you’re new to Goa, it’s worth a read before getting started here.

OAuth2 can be tricky to troubleshoot. Goa has some little features that might help. You’ll need the cGoa library for this – see the end of the article for reference details.

The package

Goa supports various scenarios such as Service accounts and JWT, but this article will mainly deal with 3 legged OAuth – when you want to get consent for access to private resources, whilst delegating the authentication to the OAuth2 service protecting an API you want to use.

Goa setup and operations revolve around a package of credentials and status (called a Goa) which are added to a property store by you, and maintained by Goa.

Setting up the credentials in Apps Script

This is a once off operation to store the credentials you’ve created in the dashboard of the developers console of the service you want to access. We’ll use the Twitter v2 API OAuth service as an example. For a complete example of setting up the Twitter API in the Twitter console (most APIS hava a silimar console and work in much the same way) see  Apps Script Oauth2 – a Goa Library refresher

Create and run a once off script

You’ll need:

  • the clientid and secret from the twitter developer console (they are sometime called consumer key and secret and various other things by different API providers
  • some name you’ll use to refer to this credential set later
  • scopes that will authorize access to the twitter resources you need to access. I’m going to use this to do twitter queries, so these are the scopes I need. You’ll find all the twitter API scopes here.
 const twitterPropertyService = () => PropertiesService.getUserProperties()
 
 cGoa.GoaApp.setPackage (twitterPropertyService (), { 
    clientId : "xxxxx",
    clientSecret : "xxxxx",
    scopes : ["tweet.read","users.read"],
    service: 'twitter',
    packageName: 'twitter'
  });
setting up apps script one off initializer

Run the script – you won’t need it again so when you’re finished testing, delete it from your script so as not to accidentially expose these credentials.

Package contents

{ clientId: 'xxxx',
  clientSecret: 'dxxxxxa',
  scopes: [ 'tweet.read', 'users.read' ],
  service: 'twitter',
  packageName: 'twitter',
  id: 'xxxx',
  revised: 1655212379600,
}
a package that hasn't yet been authenticated

Most of the content of this package was provided by you in your one off script. The id and revised properties are maintained by Goa. The serviceName matches one of Goa’s supported services. Each service has a slightly different flow and different endpoints. It’s Goa’s job to deal with all of that.

Supported services

These flows are supported at time of writing. Nowadays the google and firebase services are easier via the Apps Script manifest, but you can use Goa if you want. Goa also supports custom services – ones provide the parameters for yourself – see Goa services and customization. If you do create a custom flow, then please let me know and I’ll add it to the supported services so others can use it too.

 {
    twitter: {},
    "google_service": {},
    "google": {},
    "linkedin": {},
    "soundcloud": {},
    "podio": {},
    "shoeboxed": {},
    "github": {},
    "reddit": {},
    "asana": {},
    "live": {},
    "paypal_sandbox": {},
    "paypal_live": {},
    classy: {},
    quickbooks: {},
    firebase: {},
    vimeo: {}
  };
supported services

I can normally add new services to Goa by just adding 3 or 4 endpoint urls, and creating a service name – so don’t be shy in pinging me to add them.

Getting endpoints

Normally you wouldn’t need to do this, but you can find the endpoints and definition of these services like this, which might help if you are creating a custom service

console.log(cGoa.Service.pockage.google)

/*
{ authUrl: 'https://accounts.google.com/o/oauth2/auth',
  tokenUrl: 'https://accounts.google.com/o/oauth2/token',
  refreshUrl: 'https://accounts.google.com/o/oauth2/token',
  checkUrl: 'https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=' }
 */
example get service definition

More complicated flows

Some OAuth2 providers, like twitter, have more complex patterns, but the complications are handled by Goa service definitions.

    twitter: {
      authUrl: "https://twitter.com/i/oauth2/authorize",
      tokenUrl: "https://api.twitter.com/2/oauth2/token",
      refreshUrl: "https://api.twitter.com/2/oauth2/token",
      // twitter needs the client id/secret sent over basic authentication to get an access token back
      basic: true,
      customizeOptions: {
        // twitter has some code verification stuff
        codeVerify: (url, pockage) => {
          return `${url}${qiffyUrl(url)}code_challenge=${pockage.id}&code_challenge_method=plain`
        },
        // twitter defines offline access via a scope rather than a url parameter, 
        // so we'll just get rid of it in case its here
        // and sort it out from the consent url
        scopes: (scopes) => {
          const offline = 'offline.access'
          const online = scopes.filter(f => f !== offline)
          return {
            offline: online.concat([offline]),
            online
          }
        },
        // getting a token needs a couple of extra parameters
        token: (options = {}, pockage) => {
          const { payload = {} } = options || {}
          const newOptions = {
            ...options,
            contentType: 'application/x-www-form-urlencoded',
            payload: {
              ...payload,
              code_verifier: pockage.id,
              client_id: pockage.clientId
            }
          }
          return newOptions
        }
      }
    },
twitter OAuth2 is a more complex implementation than ost

Which Property store?

Deciding which property store to use is important and depends on how your Script is going to be used.

Here’s a guide on which property service to use.

  • If the script is to run as you – ie. access a resource belonging to you, and only you will be running the script, then use UserProperties – you’ll give access to your resource the first time you run it.
  • If the script is to run as you – ie. access a resource belonging to you, but others will be running the script accessing your resources then use ScriptProperties – you’ll give access to your resource the first time you run it.
  • If the scipt is to run as the user accessing the script – then it’s a little more complex as the credentials will need to be cloned to each new user for them to login into and give access to their resource the first time they use it while keeping individual tokens private to them. To support this scenario, initialize goa once off in ScriptProperties, and make the code below the first thing you do in your script. Goa will clone the credentials to the user’s property store the first time it sees them, will force an authentication and thereafter will maintain a separate thread of access/refresh tokens for each user.
// clone credentials for each new user it comes across to their own provate userproperty store
  cGoa.GoaApp.userClone( 
    'twitter query', 
    PropertiesService.getScriptProperties() , 
    PropertiesService.getUserProperties()
  );
  // now we can use goa as normal and it will be specific to the logged on user
  const goa = cGoa.make(
    'twitter query',
    PropertiesService.getUserProperties(),
e
  )
if script is intended for multiple users to access their own resource

If you share a script as a viewer with someone they won’t be able to see your Script properties services, but if they have full rights to the project they’ll be able to see your client credentials if they access your Properties Store. Always use the ‘least visible’ Properties Service that will work – so UserProperties is usually the best choice.

Listing package contents

If you’ve used the script property store, you can use the Project settings to see the package store entry contents. However you’ll need a script to view the UserProperty store, and of course you won’t be (and shouldn’t be) able to see the contents of other User’s property stores. To view the contents of a package, you can use a script like this

// ignore the 'e' for now -it'll be used with a webapp for a consent screent later
const makeTwitterGoa = (e) => {
  return cGoa.make(
    'twitter,
    PropertiesService.getUserProperties()
    e
  )
}

const showTwitter = () => {
  const goa = makeTwitterGoa()
  console.log(goa.getPackage())
}
show package contents

 

Consent

Before you can get an access token, the user (or you) needs to give consent for the script to access the requested resources. Goa automatically creates a webapp and asks for consent if it needs it. This usually open happens once, as Goa will use the refresh token mechanism that most providers support to silently refresh access tokens whenever necessary. Once consent has been established, Goa maintains an access token, a refresh token and an expiry time in the package, which now looks like this.

{ clientId: 'xxxx',
  clientSecret: 'xxxxx',
  scopes: [ 'tweet.read', 'users.read' ],
  service: 'twitter',
  packageName: 'twitter',
  id: 'xxxx',
  revised: 1655212379600,
  access: 
   { accessToken: 'NxxE',
     refreshToken: 'NzxxjE',
     expires: 1655219579600 } }
a consented package

Getting a token

Goa provides

  • goa.hasToken() – will return true if a token already exists in the package that can be used (hasn’t expired)
  • goa.getToken() – will return an access token. If the one in the package will expire shortly or has expired, it will get a new one via the refresh mechanism

Most of the time, there is no external access required to get a token. If Goa knows about an access token that hasn’t expired, it’ll give you that.

Forcing a refresh

You may want to force a refresh, especially if you are testing a customized flow you’ve made or just troubleshooting. Yo can do this my simply modifying the expiry date in the package in the and issuing a goa.getToken()

    const token = goa.getToken()
	
	// hack the expiry time
    const pack = goa.fetchPackage()
    pack.access.expires = new Date().getTime()
    goa.updatePackage(pack)
	
	// newToken !== token
	const newToken = goa.getToken()
	
	
forcing a refresh

Killing consent

You may want to force a re-consent dialog. You can do this either by hacking the package

    const token = goa.getToken()
	
	// hack the expiry time
    const pack = goa.fetchPackage()
    pack.access = null
    goa.updatePackage(pack)
	
	// newToken !== token
	const newToken = goa.getToken()
killing consent

or better, use the kill method


  goa.kill()
removes consent

In either case the goa will need to go through the consent process again, but the credentials will be retained – so no need to go through all that again

Remove a package

If you want to completely remove a package, including the credential, you can use the remove() method

 goa.remove()
completely remove a package

In this case you’ll need to run the one off initialization again to re-enable Goa for this service

Copying a Goa

You’ll probably want to do this if you want to have different flows for different scopes. You can of course just create a new oneoff script with a different goa name and run that.

For example, you could create ‘twitter search’ and ‘twitter post’ Goa packages with different scope requests. Another way, to avoid having credentials in your script code, is to copy an existing one, but with modified scopes.

    const oldPackage = goa.fetchPackage()
    const propertyStore = goa.getPropertyStore()
 
    cGoa.GoaApp.setPackage( propertyStore, {
      ...oldPackage,
      scopes: ["tweet.read", "users.read","tweet.likes"],
      // do this to force a new consent dialog
      access: null,
      // give it a new name
      packageName: "twitter likes"
    })
	
    const goaLike  = cGoa.make(
      "twitter likes",
      propertyStore
    )
copying an existing Goa

In this case, it’s important to ensure you specify the packageName (otherwise it’ll overwrite the old one), and to set the access to null (to provoke a new consent dialog)

Changing scopes in existing Goa

The scopes defines which of the APIs resources you’d like to access, and access tokens that are issued reflect that authorization. If you want to change the scope, you can

  • Create a new one off script to replace the current credentials and scope
  • Clone an existing Goa (as previously described)
  • Modify the existing Goa and provoke a new consent dialog – see below
    // add an extra scope and remove an existing one
    const likePackage = goaLike.fetchPackage()
    const scopes = likePackage.scopes
      // remove this one
      .filter(f => f !== "users.read")
      // add this one
      .concat(["tweet.write"])

    // force a new consent dialog
    goaLike.updatePackage({
      ...likePackage,
      scopes,
      access: null
    })
changing an existing package

fetchPackage versus getPackage

You’ll notice I’ve used both fetch and get to retrieve packages. There’s a subtle difference

getPackage

This retrieves the current package being used by Goa. It should normally only be used for reading.

fetchPackage

This returns a copy of the current package.  You can use this copy if you need to modify anything, and then use updatePackage() to replace and register your changes

Testing

I often include my test scripts as they make a handy crib sheet for how to do things and what to expect. Here are the tests for this article, plus some skeleton useful functions. You’ll need the Simple but powerful Apps Script Unit Test library to run them.

const testTwt = ({ force = false, unit } = {}) => {

  // manage skipping individual tests
  const skipTest = {
    goa: false && !force
  }

  // get a testing instance (or use the one passed over)
  unit = unit || new bmUnitTester.Unit({
    showErrorsOnly: true,
    maxLog: 100
  })

  // we'll use this token service
  const goa = makeTwitterGoa()


  unit.section(() => {
   
    unit.is(true, goa.hasToken(), {
      description: 'has a token'
    })

    const {actual: token} = unit.not(null, goa.getToken(), {
      description: 'got a token'
    })

    // force an expiry and therefore a refresh
    const pack = goa.fetchPackage()
    pack.access.expires = new Date().getTime()
    goa.updatePackage(pack)
    unit.is(false, goa.hasToken(), {
      description: 'token should be expired'
    })

    unit.not(token, goa.getToken(), {
      description: 'its a different token after refresh'
    })

    // make a copy of the package
    const oldPackage = goa.fetchPackage()
    const propertyStore = goa.getPropertyStore()

    cGoa.GoaApp.setPackage(propertyStore, {
      ...oldPackage,
      scopes: ["tweet.read", "users.read", "tweet.likes"],
      // do this to force a new consent dialog
      access: null,
      // give it a new name
      packageName: "twitter likes"
    })

    const goaLike = cGoa.make(
      "twitter likes",
      propertyStore
    )

    unit.is(false, goaLike.hasToken(), {
      description: 'should be no token'
    })

    // add an extra scope and remove an existing one
    const likePackage = goaLike.fetchPackage()
    const scopes = likePackage.scopes
      // remove this one
      .filter(f => f !== "users.read")
      // add this one
      .concat(["tweet.write"])

    // force a new consent dialog
    goaLike.updatePackage({
      ...likePackage,
      scopes,
      access: null
    })

    unit.is(scopes, goaLike.getPackage().scopes, {
      description: 'scopes changed successfully'
    })

    unit.is(false, goaLike.hasToken() , {
      description: 'updated package has no token available'
    })

  }, {
    description: 'test goa service',
    skip: skipTest.goa
  })

}

const makeTwitterGoa = (e) => {
  // my credentials for one off initial set up are stored elsewhere
  const { twitter } = SETTINGS
  return cGoa.make(
    twitter.name,
    twitter.propertyService,
    e
  )
}

const oneoffTwitter = () => {
  const { twitter } = SETTINGS
  cGoa.GoaApp.setPackage(twitter.propertyService, twitter.goaPackage)
}

function doGet(e) {
  return doGeTwitter(e)
}

// run this once off to get authorized
function doGeTwitter(e) {

  const goa = makeTwitterGoa(e)

  // it's possible that we need consent - this will cause a consent dialog
  if (goa.needsConsent()) {
    return goa.getConsent();
  }

  // get a token
  const token = goa.getToken()

  // if we get here its time for your webapp to run and we should have a token, or thrown an error somewhere
  if (!goa.hasToken()) throw 'something went wrong with goa - did you check if consent was needed?';

  // now we can use the token in a query or just leave it there registered for future server side use
  return HtmlService.createHtmlOutput(`Got this access token ${token}`)

}
testing and utility

Links

cGoa library 1v_l4xN3ICa0lAW315NQEzAHPSoNiFdWHsMEwj2qA5t9cgZ5VWci2Qxv2

IDE

Github

bmUnitTester: 1zOlHMOpO89vqLPe5XpC-wzA9r5yaBkWt_qFjKqFNsIZtNJ-iUjBYDt-x

IDE

GitHub

Related

Goa Oauth2 for Apps Script

Apps Script Oauth2 library Goa: tips, tricks and hacks

Motivation Goa is a library to support OAuth2 for Apps Script connecting to a variety of services, using a variety ...
Read More
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

Goa v8 changes and enhancements

v8 and other HTML service changes meant I had to make a few small changes to cGoa. The good news it's ...
Read More

Goa in a sidebar

This describes how to implement goa in a sidebar. The example will assume that the authorization process should be repeated ...
Read More

Automatic UI support for OAUTH2 Goa dialogs

There's always a little bit of work needed if you are planning to do an OAUTH2 flow that might need ...
Read More

How OAuth2 works and Goa implementation

Oauth2 for Apps Script in a few lines of code (which you should read first for background) has many pages of ...
Read More

Using multiple service accounts to work across projects and lock down permissions with Apps Script and Goa

I use Cloud storage quite extensively as it's a great way to share data across platforms and projects. Apps Script ...
Read More

Hitching a ride on Goa’s property store

Goa is a library to simplify Oauth2 with both Google services and other Oauth2 providers, many of which are natively ...
Read More

Adding custom services to goa

Goa knows how to talk to a number of different Oauth2 providers using various flavours and varieties of OAuth flow ...
Read More

Google Apps Script Oauth2 for Vimeo with Goa

The Vimeo Rest API is a simple, standard API. It's been added to the Goa library list of services. Goa ...
Read More

Google Apps Script Oauth2 for quickbooks with Goa

The quickbooks Rest API is a simple, standard API. It's been added to the Goa library list of services. Goa ...
Read More

Goa and the Classy API

The Classy Rest API uses OAuth2 a little like a Service Account, which means there's typically no user dialog. It uses a ...
Read More

Google Datastore service for Goa using service account examples

This describes how to authenticate with Google Datastore using Goa along with a service account, as described in Oauth2 for Apps ...
Read More

Shoeboxed service for Goa examples

This describes how to authenticate with Podio using Goa, as described in Oauth2 for Apps Script in a few lines of ...
Read More

Reddit service for Goa examples

This describes how to authenticate with Podio using Goa, as described in Oauth2 for Apps Script in a few lines of ...
Read More

Podio service for Goa examples

This describes how to authenticate with Podio using Goa, as described in Oauth2 for Apps Script in a few lines of ...
Read More

Microsoft Live service for Goa Examples

This describes how to authenticate with Microsoft Live using Goa, as described in Oauth2 for Apps Script in a few lines ...
Read More

Soundcloud service for Goa examples

This describes how to authenticate with Soundcloud using Goa, as described in Oauth2 for Apps Script in a few lines ...
Read More

Google Datastore service for Goa examples

This describes how to authenticate with Google Datastore using Goa, as described in Oauth2 for Apps Script in a few lines ...
Read More

Asana service for Goa examples

This describes how to use the Asana service to authenticate with Asana using Goa, as described in Oauth2 for Apps Script ...
Read More
Goa Oauth2 for Apps Script

Apps Script Oauth2 library Goa: tips, tricks and hacks

Motivation Goa is a library to support OAuth2 for Apps Script connecting to a variety of services, using a variety ...
Read More
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